refactor: add to template agent roles

This commit is contained in:
Petr Mironychev
2026-06-04 16:21:34 +02:00
parent c151c5030b
commit 3179c0c358
113 changed files with 383 additions and 5292 deletions

View File

@@ -9,6 +9,7 @@ add_library(Session STATIC
Session.hpp Session.cpp
SessionManager.hpp SessionManager.cpp
SystemPromptBuilder.hpp SystemPromptBuilder.cpp
ToolContributorRegistry.hpp
)
target_link_libraries(Session

View File

@@ -210,7 +210,9 @@ LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx)
if (!provider->prepareRequest(payload, tmpl, ctx, /*tools=*/false, /*thinking=*/false))
return {};
const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint);
QString endpoint = cfg.endpoint;
endpoint.replace(QStringLiteral("${MODEL}"), cfg.model);
const auto id = provider->sendRequest(QUrl(provider->url()), payload, endpoint);
if (id.isEmpty())
return {};
@@ -242,7 +244,9 @@ LLMQore::RequestID Session::dispatch(
if (!provider->prepareRequest(payload, tmpl, ctx, tools, thinking))
return {};
const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint);
QString endpoint = cfg.endpoint;
endpoint.replace(QStringLiteral("${MODEL}"), cfg.model);
const auto id = provider->sendRequest(QUrl(provider->url()), payload, endpoint);
if (id.isEmpty())
return {};

View File

@@ -9,6 +9,8 @@
#include <QPointer>
#include <QString>
#include "ToolContributorRegistry.hpp"
namespace QodeAssist {
class AgentFactory;
@@ -41,6 +43,9 @@ public:
void cancelAll();
ToolContributorRegistry &toolContributors() noexcept { return m_toolContributors; }
const ToolContributorRegistry &toolContributors() const noexcept { return m_toolContributors; }
signals:
void sessionCreated(Session *session);
void sessionRemoved(Session *session);
@@ -48,6 +53,7 @@ signals:
private:
QPointer<AgentFactory> m_agentFactory;
QList<QPointer<Session>> m_sessions;
ToolContributorRegistry m_toolContributors;
};
} // namespace QodeAssist

View File

@@ -0,0 +1,41 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <functional>
#include <utility>
#include <vector>
namespace LLMQore {
class ToolsManager;
}
namespace QodeAssist {
class ToolContributorRegistry
{
public:
using Contributor = std::function<void(LLMQore::ToolsManager *)>;
void add(Contributor contributor)
{
if (contributor)
m_contributors.push_back(std::move(contributor));
}
void contribute(LLMQore::ToolsManager *tools) const
{
if (!tools)
return;
for (const auto &contributor : m_contributors)
contributor(tools);
}
void clear() { m_contributors.clear(); }
private:
std::vector<Contributor> m_contributors;
};
} // namespace QodeAssist

View File

@@ -248,6 +248,15 @@ void AgentFactory::setModelOverride(const QString &agentName, const QString &mod
saveModelOverrides();
}
QString AgentFactory::effectiveModel(const QString &agentName) const
{
const QString ov = m_modelOverrides.value(agentName);
if (!ov.isEmpty())
return ov;
const AgentConfig *cfg = configByName(agentName);
return cfg ? cfg->model : QString();
}
namespace {
QString modelOverridesPath()
{

View File

@@ -59,6 +59,10 @@ public:
[[nodiscard]] QString modelOverride(const QString &agentName) const;
void setModelOverride(const QString &agentName, const QString &model);
// The model that will actually be sent for this agent: the settings
// override if set, otherwise the agent TOML's default `model`.
[[nodiscard]] QString effectiveModel(const QString &agentName) const;
[[nodiscard]] Providers::ProviderInstanceFactory *instanceFactory() const noexcept;
[[nodiscard]] Providers::ProviderSecretsStore *secretsStore() const noexcept;

View File

@@ -8,8 +8,12 @@
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStringList>
#include <coreplugin/icore.h>
#include <filesystem>
#include <inja/inja.hpp>
@@ -155,6 +159,45 @@ void registerStringHelpers(inja::Environment &env)
});
}
// Read a role's system prompt from the role JSON written by the settings Roles
// UI (AgentRolesManager). Returns "" if the role doesn't exist.
std::string roleSystemPrompt(const QString &id)
{
if (id.isEmpty())
return {};
const QString path
= Core::ICore::userResourcePath(
QStringLiteral("qodeassist/agent_roles/%1.json").arg(id))
.toFSPathString();
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning("[QodeAssist] agent_role: role '%s' not found at %s",
qUtf8Printable(id), qUtf8Printable(path));
return {};
}
return QJsonDocument::fromJson(f.readAll())
.object()
.value("systemPrompt")
.toString()
.toStdString();
}
// Building blocks for composing a profile's `system_prompt` (alongside
// read_file/file_exists):
// {{ agent_role() }} — the runtime-selected role (Bindings.roleId, which
// the chat sets; falls back to "developer")
// {{ agent_role("<id>") }} — a specific role by id
void registerAgentRole(inja::Environment &env, const Bindings &b)
{
const QString runtimeRole = b.roleId.isEmpty() ? QStringLiteral("developer") : b.roleId;
env.add_callback("agent_role", 0, [runtimeRole](inja::Arguments &) -> nlohmann::json {
return roleSystemPrompt(runtimeRole);
});
env.add_callback("agent_role", 1, [](inja::Arguments &args) -> nlohmann::json {
return roleSystemPrompt(QString::fromStdString(args.at(0)->get<std::string>()));
});
}
void registerSandbox(inja::Environment &env)
{
@@ -183,6 +226,7 @@ QString render(const QString &templateSource, const Bindings &bindings, QString
registerFileExists(env, bindings);
registerReadDir(env, bindings);
registerStringHelpers(env);
registerAgentRole(env, bindings);
inja::Template tpl;
try {

View File

@@ -12,6 +12,9 @@ struct Bindings
{
QString projectDir;
QString homeDir;
// Role id selected at runtime (e.g. in the chat). Used by the no-arg
// `{{ agent_role() }}` template callback; empty falls back to "developer".
QString roleId;
};
QString render(const QString &templateSource, const Bindings &bindings,

View File

@@ -12,7 +12,6 @@
<file>partials/google_part.jinja</file>
<file>partials/ollama_messages.jinja</file>
<file>chat_base.toml</file>
<file>openai_base_chat.toml</file>
<file>openai_responses_base.toml</file>
<file>anthropic_base_chat.toml</file>

View File

@@ -3,16 +3,18 @@ schema_version = 1
name = "Anthropic Base Chat"
description = "Anthropic Messages API request body (/v1/messages). Abstract — extend it and set model."
abstract = true
extends = "Chat Base"
provider_instance = "Claude"
endpoint = "/v1/messages"
enable_tools = true
tags = ["chat", "claude", "anthropic", "cloud"]
system_prompt = """{{ agent_role() }}"""
[body]
max_tokens = 8192
temperature = 1
stream = true
system = """{% if existsIn(ctx, "system_prompt") %}{{ tojson(ctx.system_prompt) }}{% endif %}"""
messages = """
[ {% include "partials/anthropic_messages.jinja" %} ]

View File

@@ -1,16 +0,0 @@
schema_version = 1
name = "Chat Base"
description = "Shared system prompt for coding-chat agents. Abstract — not selectable."
abstract = true
system_prompt = """
You are a helpful coding assistant integrated into Qt Creator.
Answer concisely. Prefer concrete diffs or minimal patches over rewriting
whole files. Use markdown code blocks with language tags.
{% if file_exists("${PROJECT_DIR}/README.md") %}
## Project README.md
{{ read_file("${PROJECT_DIR}/README.md") }}
{% endif %}
"""

View File

@@ -3,12 +3,13 @@ schema_version = 1
name = "Google Base Chat"
description = "Google Gemini generateContent request body. Abstract — extend it and set model/endpoint."
abstract = true
extends = "Chat Base"
provider_instance = "Google AI"
enable_tools = true
tags = ["chat", "gemini", "google", "cloud"]
system_prompt = """{{ agent_role() }}"""
[body]
system_instruction = """{% if existsIn(ctx, "system_prompt") %}{ "parts": [ { "text": {{ tojson(ctx.system_prompt) }} } ] }{% endif %}"""
contents = """

View File

@@ -4,7 +4,7 @@ extends = "Google Base Chat"
name = "Gemini Chat"
description = "Google Gemini 2.5 Flash (generateContent API) — coding chat with thinking."
endpoint = "/models/gemini-2.5-flash:streamGenerateContent?alt=sse"
endpoint = "/models/${MODEL}:streamGenerateContent?alt=sse"
model = "gemini-2.5-flash"
enable_thinking = true

View File

@@ -3,12 +3,13 @@ schema_version = 1
name = "Ollama Base Chat"
description = "Ollama native /api/chat request body. Abstract — extend it and set model/options."
abstract = true
extends = "Chat Base"
provider_instance = "Ollama (Native)"
endpoint = "/api/chat"
tags = ["ollama", "local"]
system_prompt = """{{ agent_role() }}"""
[body]
stream = true
messages = """

View File

@@ -3,16 +3,18 @@ schema_version = 1
name = "OpenAI Base Chat"
description = "OpenAI Chat Completions request body. Abstract — extend it and set provider/endpoint/model."
abstract = true
extends = "Chat Base"
provider_instance = "OpenAI (Chat Completions)"
endpoint = "/chat/completions"
enable_tools = true
tags = ["chat", "openai", "cloud"]
system_prompt = """{{ agent_role() }}"""
[body]
max_tokens = 8192
temperature = 0.7
stream = true
messages = """
[ {% include "partials/openai_messages.jinja" %} ]
"""

View File

@@ -3,16 +3,18 @@ schema_version = 1
name = "OpenAI Responses Base"
description = "OpenAI Responses API request body (/responses). Abstract — extend it and set provider/endpoint/model."
abstract = true
extends = "Chat Base"
provider_instance = "OpenAI (Responses API)"
endpoint = "/responses"
enable_tools = true
tags = ["chat", "openai", "responses", "cloud"]
system_prompt = """{{ agent_role() }}"""
[body]
max_output_tokens = 8192
temperature = 0.7
stream = true
instructions = """{% if existsIn(ctx, "system_prompt") %}{{ tojson(ctx.system_prompt) }}{% endif %}"""
input = """
[ {% include "partials/openai_responses_input.jinja" %} ]

View File

@@ -2,6 +2,7 @@ add_library(Common INTERFACE)
target_sources(Common INTERFACE
ContextData.hpp
ResponseCleaner.hpp
)
target_include_directories(Common INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -0,0 +1,103 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QString>
#include <QStringList>
#include <QRegularExpression>
namespace QodeAssist {
class ResponseCleaner
{
public:
static QString clean(const QString &response)
{
QString cleaned = removeCodeBlocks(response);
cleaned = trimWhitespace(cleaned);
cleaned = removeExplanations(cleaned);
return cleaned;
}
private:
static QString removeCodeBlocks(const QString &text)
{
if (!text.contains("```")) {
return text;
}
QRegularExpression codeBlockRegex("```\\w*\\n([\\s\\S]*?)```");
QRegularExpressionMatch match = codeBlockRegex.match(text);
if (match.hasMatch()) {
return match.captured(1);
}
int firstFence = text.indexOf("```");
int lastFence = text.lastIndexOf("```");
if (firstFence != -1 && lastFence > firstFence) {
int firstNewLine = text.indexOf('\n', firstFence);
if (firstNewLine != -1) {
return text.mid(firstNewLine + 1, lastFence - firstNewLine - 1);
}
}
return text;
}
static QString trimWhitespace(const QString &text)
{
QString result = text;
while (result.startsWith('\n') || result.startsWith('\r')) {
result = result.mid(1);
}
while (result.endsWith('\n') || result.endsWith('\r')) {
result.chop(1);
}
return result;
}
static QString removeExplanations(const QString &text)
{
static const QStringList explanationPrefixes = {
"here's the", "here is the", "here's", "here is",
"the refactored", "refactored code:", "code:",
"i've refactored", "i refactored", "i've changed", "i changed"
};
QStringList lines = text.split('\n');
int startLine = 0;
for (int i = 0; i < qMin(3, lines.size()); ++i) {
QString line = lines[i].trimmed().toLower();
bool isExplanation = false;
for (const QString &prefix : explanationPrefixes) {
if (line.startsWith(prefix) || line.contains(prefix + " code")) {
isExplanation = true;
break;
}
}
if (line.length() < 50 && line.endsWith(':')) {
isExplanation = true;
}
if (isExplanation) {
startLine = i + 1;
} else if (!line.isEmpty()) {
break;
}
}
if (startLine > 0 && startLine < lines.size()) {
lines = lines.mid(startLine);
return lines.join('\n');
}
return text;
}
};
} // namespace QodeAssist

View File

@@ -199,6 +199,7 @@ public:
AgentRosterRow(int index,
const QString &name,
const AgentConfig *cfg,
const QString &effectiveModel,
bool active,
bool first,
bool last,
@@ -226,6 +227,7 @@ private:
AgentRosterRow::AgentRosterRow(int index,
const QString &name,
const AgentConfig *cfg,
const QString &effectiveModel,
bool active,
bool first,
bool last,
@@ -282,7 +284,7 @@ AgentRosterRow::AgentRosterRow(int index,
body->setSpacing(2);
const QString displayName = cfg ? cfg->name : tr("%1 (missing)").arg(name);
const QString model = cfg ? cfg->model : QString();
const QString model = effectiveModel;
const bool isUser = cfg && cfg->isUserSource();
body->addWidget(buildIdentityLine(displayName, model, active, isUser, theme));
@@ -562,9 +564,11 @@ void AgentRosterWidget::rebuildRows()
for (int i = 0; i < m_names.size(); ++i) {
const QString &name = m_names.at(i);
const AgentConfig *cfg = m_factory ? m_factory->configByName(name) : nullptr;
const QString effModel = m_factory ? m_factory->effectiveModel(name) : QString();
auto *row = new AgentRosterRow(i,
name,
cfg,
effModel,
i == m_activeIndex,
/*first*/ i == 0,
/*last*/ i == m_names.size() - 1,