mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-15 18:59:13 -04:00
refactor: Add agents for providers
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
77
sources/agents/claude_sonnet_chat.toml
Normal file
77
sources/agents/claude_sonnet_chat.toml
Normal 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
|
||||
79
sources/agents/google_gemini_chat.toml
Normal file
79
sources/agents/google_gemini_chat.toml
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user