refactor: Add agents for providers

This commit is contained in:
Petr Mironychev
2026-06-02 01:10:29 +02:00
parent 6220308a93
commit 98a618cf87
15 changed files with 238 additions and 127 deletions

View File

@@ -125,7 +125,8 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<LLMQore::TextContent>(buildCompressionPrompt()));
m_currentRequestId = session->send(std::move(blocks), /*toolsOverride=*/false);
m_currentRequestId = session->send(
std::move(blocks), /*toolsOverride=*/false, /*thinkingOverride=*/false);
if (m_currentRequestId.isEmpty()) {
handleCompressionError(tr("Failed to start compression request"));
return;

View File

@@ -167,6 +167,16 @@ ChatRootView::ChatRootView(QQuickItem *parent)
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::isThinkingSupportChanged);
connect(
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useToolsChanged);
connect(
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useThinkingChanged);
auto editors = Core::EditorManager::instance();
@@ -292,7 +302,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) {
PendingSend p = m_pendingSend;
m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
dispatchSend(p.message, p.attachments, p.linkedFiles);
}
});
@@ -305,7 +315,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) {
PendingSend p = m_pendingSend;
m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
dispatchSend(p.message, p.attachments, p.linkedFiles);
}
});
}
@@ -426,21 +436,17 @@ void ChatRootView::sendMessage(const QString &message)
{
const QStringList attachments = m_attachmentFiles;
const QStringList linkedFiles = m_linkedFiles;
const bool tools = useTools();
const bool thinking = useThinking();
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking))
if (deferSendForAutoCompress(message, attachments, linkedFiles))
return;
dispatchSend(message, attachments, linkedFiles, tools, thinking);
dispatchSend(message, attachments, linkedFiles);
}
bool ChatRootView::deferSendForAutoCompress(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useToolsArg,
bool useThinkingArg)
const QStringList &linkedFiles)
{
auto &settings = Settings::chatAssistantSettings();
if (!settings.autoCompress())
@@ -466,7 +472,7 @@ bool ChatRootView::deferSendForAutoCompress(
.arg(inputTokens)
.arg(threshold));
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
m_pendingSend = {message, attachments, linkedFiles, true};
compressCurrentChat();
return true;
}
@@ -474,9 +480,7 @@ bool ChatRootView::deferSendForAutoCompress(
void ChatRootView::dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useToolsArg,
bool useThinkingArg)
const QStringList &linkedFiles)
{
if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments);
@@ -497,7 +501,7 @@ void ChatRootView::dispatchSend(
m_clientInterface->setSkillsManager(skillsManager());
m_clientInterface->setSessionManager(sessionManager());
m_clientInterface->setActiveAgent(currentChatAgent());
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
m_clientInterface->sendMessage(message, attachments, linkedFiles);
m_fileManager->clearIntermediateStorage();
clearAttachmentFiles();
@@ -1112,24 +1116,12 @@ QString ChatRootView::lastErrorMessage() const
bool ChatRootView::useTools() const
{
return Settings::chatAssistantSettings().enableChatTools();
}
void ChatRootView::setUseTools(bool enabled)
{
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
return m_agentController->currentSupportsTools();
}
bool ChatRootView::useThinking() const
{
return Settings::chatAssistantSettings().enableThinkingMode();
}
void ChatRootView::setUseThinking(bool enabled)
{
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
return m_agentController->currentSupportsThinking();
}
void ChatRootView::applyFileEdit(const QString &editId)

View File

@@ -48,8 +48,8 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
@@ -136,9 +136,7 @@ public:
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
bool useTools() const;
void setUseTools(bool enabled);
bool useThinking() const;
void setUseThinking(bool enabled);
Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId);
@@ -229,15 +227,11 @@ private:
bool deferSendForAutoCompress(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
const QStringList &linkedFiles);
void dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
const QStringList &linkedFiles);
bool hasImageAttachments(const QStringList &attachments) const;
SessionFileRegistry *sessionFileRegistry() const;
@@ -256,8 +250,6 @@ private:
QString message;
QStringList attachments;
QStringList linkedFiles;
bool useTools = false;
bool useThinking = false;
bool active = false;
};
PendingSend m_pendingSend;

View File

@@ -78,12 +78,8 @@ void ClientInterface::setActiveAgent(const QString &agentName)
void ClientInterface::sendMessage(
const QString &message,
const QList<QString> &attachments,
const QList<QString> &linkedFiles,
bool useTools,
bool useThinking)
const QList<QString> &linkedFiles)
{
Q_UNUSED(useThinking)
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message");
return;
@@ -256,7 +252,7 @@ void ClientInterface::sendMessage(
}
}
const LLMQore::RequestID requestId = session->send(std::move(blocks), useTools);
const LLMQore::RequestID requestId = session->send(std::move(blocks));
if (requestId.isEmpty()) {
const QString error = QStringLiteral("Failed to start chat request for agent: %1")
.arg(m_activeAgent);

View File

@@ -41,9 +41,7 @@ public:
void sendMessage(
const QString &message,
const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {},
bool useTools = false,
bool useThinking = false);
const QList<QString> &linkedFiles = {});
void clearMessages();
void cancelRequest();

View File

@@ -138,19 +138,6 @@ ChatRootView {
relocateTooltip.text: (typeof _chatview !== 'undefined')
? qsTr("Move this chat to an editor tab")
: qsTr("Move this chat to a separate window")
toolsButton {
checked: root.useTools
onCheckedChanged: {
root.useTools = toolsButton.checked
}
}
thinkingMode {
checked: root.useThinking
enabled: root.isThinkingSupport
onCheckedChanged: {
root.useThinking = thinkingMode.checked
}
}
settingsButton.onClicked: root.openSettings()
agentSelector {
model: root.availableChatAgents

View File

@@ -23,8 +23,6 @@ Rectangle {
property alias pinButton: pinButtonId
property alias relocateButton: relocateButtonId
property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId
property alias agentSelector: agentSelectorId
property alias relocateTooltip: relocateTooltipId
@@ -151,62 +149,6 @@ Rectangle {
Row {
spacing: 10
QoAButton {
id: toolsButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: toolsButtonId.hovered
delay: 250
text: {
if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return toolsButtonId.checked
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Tools disabled: Simple conversation without tool access")
}
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: thinkingModeId.hovered
delay: 250
text: thinkingModeId.enabled
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
}
QoAButton {
id: settingsButtonId

View File

@@ -153,7 +153,8 @@ LLMQore::RequestID Session::sendText(const QString &text)
LLMQore::RequestID Session::send(
std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks,
std::optional<bool> toolsOverride)
std::optional<bool> toolsOverride,
std::optional<bool> thinkingOverride)
{
if (!isValid() || userBlocks.empty())
return {};
@@ -168,7 +169,7 @@ LLMQore::RequestID Session::send(
msg.appendBlock(std::move(b));
m_history->append(std::move(msg));
return dispatch(toolsOverride);
return dispatch(toolsOverride, thinkingOverride);
}
void Session::cancel()
@@ -221,7 +222,8 @@ LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx)
return id;
}
LLMQore::RequestID Session::dispatch(std::optional<bool> toolsOverride)
LLMQore::RequestID Session::dispatch(
std::optional<bool> toolsOverride, std::optional<bool> thinkingOverride)
{
auto *provider = m_agent->provider();
auto *tmpl = m_agent->promptTemplate();
@@ -237,7 +239,8 @@ LLMQore::RequestID Session::dispatch(std::optional<bool> toolsOverride)
QJsonObject payload{{QStringLiteral("model"), cfg.model}};
const bool tools = toolsOverride.value_or(cfg.enableTools);
if (!provider->prepareRequest(payload, tmpl, ctx, tools, cfg.enableThinking))
const bool thinking = thinkingOverride.value_or(cfg.enableThinking);
if (!provider->prepareRequest(payload, tmpl, ctx, tools, thinking))
return {};
const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint);
@@ -285,6 +288,9 @@ Templates::ContextData Session::buildLegacyContext(
QVector<LegacyMessage> hist;
for (const auto &m : history) {
if (m.role() == Message::Role::System)
continue;
QVector<ContentBlockEntry> blockEntries;
for (const auto &blockPtr : m.blocks()) {
@@ -329,12 +335,17 @@ Templates::ContextData Session::buildLegacyContext(
.arg(sa->fileName(), text);
blockEntries.append(std::move(e));
} else if (auto *th = dynamic_cast<LLMQore::ThinkingContent *>(block)) {
// Claude rejects thinking blocks replayed without a signature.
if (th->signature().isEmpty())
continue;
ContentBlockEntry e;
e.kind = ContentBlockEntry::Kind::Thinking;
e.thinking = th->thinking();
e.signature = th->signature();
blockEntries.append(std::move(e));
} else if (auto *rth = dynamic_cast<LLMQore::RedactedThinkingContent *>(block)) {
if (rth->signature().isEmpty())
continue;
ContentBlockEntry e;
e.kind = ContentBlockEntry::Kind::RedactedThinking;
e.signature = rth->signature();

View File

@@ -64,7 +64,8 @@ public:
LLMQore::RequestID send(
std::vector<std::unique_ptr<LLMQore::ContentBlock>> userBlocks,
std::optional<bool> toolsOverride = std::nullopt);
std::optional<bool> toolsOverride = std::nullopt,
std::optional<bool> thinkingOverride = std::nullopt);
LLMQore::RequestID sendText(const QString &text);
@@ -83,7 +84,9 @@ private slots:
void onRouterEvent(const QodeAssist::ResponseEvent &ev);
private:
LLMQore::RequestID dispatch(std::optional<bool> toolsOverride = std::nullopt);
LLMQore::RequestID dispatch(
std::optional<bool> toolsOverride = std::nullopt,
std::optional<bool> thinkingOverride = std::nullopt);
Templates::ContextData toLegacyContext() const;
Agent *m_agent = nullptr; // child if non-null

View File

@@ -5,5 +5,7 @@
<file>ollama_gemma4_e4b_chat.toml</file>
<file>ollama_codellama_7b_code_fim.toml</file>
<file>ollama_codellama_13b_qml_fim.toml</file>
<file>claude_sonnet_chat.toml</file>
<file>google_gemini_chat.toml</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,77 @@
schema_version = 1
name = "Claude Sonnet Chat"
description = "Anthropic Claude (Messages API) — coding chat assistant via the hosted Claude provider."
provider_instance = "Claude"
endpoint = "/v1/messages"
model = "claude-sonnet-4-6"
role = """
You are a helpful coding assistant integrated into Qt Creator.
Answer concisely. When the user shares code, prefer concrete diffs or
minimal patches over rewriting whole files. Use markdown code blocks
with language tags so the IDE can render them.
"""
enable_thinking = true
enable_tools = true
tags = ["chat", "claude", "anthropic", "cloud"]
context = """
{%- set readme = read_file("${PROJECT_DIR}/README.md") -%}
{%- if length(readme) > 0 %}
## Project README.md
{{ readme }}
{%- endif %}
"""
[template]
message_format = """
{
{%- if existsIn(ctx, "system_prompt") %}
"system": {{ tojson(ctx.system_prompt) }},
{%- endif %}
"messages": [
{%- for msg in ctx.history %}
{
"role": {{ tojson(msg.role) }},
"content": [
{%- for b in msg.content_blocks %}
{%- if b.type == "image" %}
{
"type": "image",
"source": {
{%- if b.is_url %}
"type": "url",
"url": {{ tojson(b.data) }}
{%- else %}
"type": "base64",
"media_type": {{ tojson(b.media_type) }},
"data": {{ tojson(b.data) }}
{%- endif %}
}
}{% if not loop.is_last %},{% endif %}
{%- else %}
{{ tojson(b) }}{% if not loop.is_last %},{% endif %}
{%- endif %}
{%- endfor %}
]
}{% if not loop.is_last %},{% endif %}
{%- endfor %}
]
}
"""
[template.sampling]
max_tokens = 8192
temperature = 1
[template.thinking.overrides]
temperature = 1
[template.thinking.request_block.thinking]
type = "enabled"
budget_tokens = 4096

View File

@@ -0,0 +1,79 @@
schema_version = 1
name = "Gemini Chat"
description = "Google Gemini (generateContent API) — coding chat assistant via the hosted Google AI provider."
provider_instance = "Google AI"
endpoint = "/models/gemini-2.5-flash:streamGenerateContent?alt=sse"
model = "gemini-2.5-flash"
role = """
You are a helpful coding assistant integrated into Qt Creator.
Answer concisely. When the user shares code, prefer concrete diffs or
minimal patches over rewriting whole files. Use markdown code blocks
with language tags so the IDE can render them.
"""
enable_thinking = true
enable_tools = true
tags = ["chat", "gemini", "google", "cloud"]
context = """
{%- set readme = read_file("${PROJECT_DIR}/README.md") -%}
{%- if length(readme) > 0 %}
## Project README.md
{{ readme }}
{%- endif %}
"""
[template]
message_format = """
{
{%- if existsIn(ctx, "system_prompt") %}
"system_instruction": { "parts": [{ "text": {{ tojson(ctx.system_prompt) }} }] },
{%- endif %}
"contents": [
{%- for msg in ctx.history %}
{
"role": {% if msg.role == "assistant" %}"model"{% else %}"user"{% endif %},
"parts": [
{%- for b in msg.content_blocks %}
{%- if b.type == "text" %}
{ "text": {{ tojson(b.text) }} }
{%- else if b.type == "thinking" %}
{ "text": {{ tojson(b.thinking) }}, "thought": true, "thoughtSignature": {{ tojson(b.signature) }} }
{%- else if b.type == "tool_use" %}
{ "functionCall": { "name": {{ tojson(b.name) }}, "args": {{ tojson(b.input) }} } }
{%- else if b.type == "tool_result" %}
{ "functionResponse": { "name": {{ tojson(b.name) }}, "response": { "result": {{ tojson(b.content) }} } } }
{%- else if b.type == "image" %}
{%- if b.is_url %}
{ "file_data": { "mime_type": {{ tojson(b.media_type) }}, "file_uri": {{ tojson(b.data) }} } }
{%- else %}
{ "inline_data": { "mime_type": {{ tojson(b.media_type) }}, "data": {{ tojson(b.data) }} } }
{%- endif %}
{%- else %}
{ "text": "" }
{%- endif %}
{% if not loop.is_last %},{% endif %}
{%- endfor %}
]
}{% if not loop.is_last %},{% endif %}
{%- endfor %}
]
}
"""
[template.sampling.generationConfig]
maxOutputTokens = 8192
temperature = 1
[template.thinking.request_block.generationConfig]
temperature = 1
maxOutputTokens = 16000
[template.thinking.request_block.generationConfig.thinkingConfig]
includeThoughts = true
thinkingBudget = 8192

View File

@@ -5,6 +5,8 @@
#include <utility>
#include <QJsonObject>
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/ClaudeClient.hpp>
#include <LLMQore/GoogleAIClient.hpp>
@@ -58,6 +60,20 @@ QFuture<QList<QString>> GenericProvider::getInstalledModels(const QString &url)
return m_client->listModels();
}
RequestID GenericProvider::sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint)
{
// Gemini carries the model in the URL and rejects unknown body fields, so
// the model/stream keys injected by the generic pipeline must be dropped.
if (m_id == ProviderID::GoogleAI) {
QJsonObject cleaned = payload;
cleaned.remove("model");
cleaned.remove("stream");
return Provider::sendRequest(url, cleaned, endpoint);
}
return Provider::sendRequest(url, payload, endpoint);
}
namespace {
using Cap = ProviderCapability;

View File

@@ -36,6 +36,9 @@ public:
ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
RequestID sendRequest(
const QUrl &url, const QJsonObject &payload, const QString &endpoint) override;
private:
QString m_name;
ProviderID m_id;

View File

@@ -5,6 +5,7 @@
#include "JsonPromptTemplate.hpp"
#include <QDebug>
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
@@ -42,6 +43,16 @@ nlohmann::json buildContextJson(const ContextData &context)
ctx["files_metadata"] = std::move(files);
}
// tool_result blocks only carry the tool_use_id; resolve the originating
// tool name so templates (e.g. Google's functionResponse.name) can emit it.
QHash<QString, QString> toolNameById;
if (context.history) {
for (const auto &msg : context.history.value())
for (const auto &b : msg.blocks)
if (b.kind == ContentBlockEntry::Kind::ToolUse)
toolNameById.insert(b.toolUseId, b.toolName);
}
nlohmann::json history = nlohmann::json::array();
if (context.history) {
for (const auto &msg : context.history.value()) {
@@ -93,6 +104,7 @@ nlohmann::json buildContextJson(const ContextData &context)
bj["type"] = "tool_result";
bj["tool_use_id"] = b.toolUseId.toStdString();
bj["content"] = b.result.toStdString();
bj["name"] = toolNameById.value(b.toolUseId).toStdString();
break;
case ContentBlockEntry::Kind::Image:
bj["type"] = "image";