mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-14 02:09:22 -04:00
refactor: add to template agent roles
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
41
sources/Session/ToolContributorRegistry.hpp
Normal file
41
sources/Session/ToolContributorRegistry.hpp
Normal 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
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" %} ]
|
||||
|
||||
@@ -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 %}
|
||||
"""
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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" %} ]
|
||||
"""
|
||||
|
||||
@@ -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" %} ]
|
||||
|
||||
@@ -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})
|
||||
|
||||
103
sources/common/ResponseCleaner.hpp
Normal file
103
sources/common/ResponseCleaner.hpp
Normal 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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user