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

@@ -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";