mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-24 04:01:04 -04:00
feat: add support acp in common chat (#369)
This commit is contained in:
@@ -6,14 +6,18 @@
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <QPromise>
|
||||
|
||||
#include "ChatSerializer.hpp"
|
||||
#include <memory>
|
||||
|
||||
#include "ChatFileStore.hpp"
|
||||
#include "llmcore/ContextData.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "providers/ProvidersManager.hpp"
|
||||
#include "session/FencedText.hpp"
|
||||
#include "session/FileEditPayload.hpp"
|
||||
#include "session/HistoryProjection.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ToolsSettings.hpp"
|
||||
#include "tools/ReadOriginalHistoryTool.hpp"
|
||||
@@ -24,8 +28,16 @@ namespace QodeAssist::Chat {
|
||||
LlmChatBackend::LlmChatBackend(Templates::IPromptProvider *promptProvider, QObject *parent)
|
||||
: Session::ChatBackend(parent)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_providerResolver([](const QString &name) {
|
||||
return Providers::ProvidersManager::instance().getProviderByName(name);
|
||||
})
|
||||
{}
|
||||
|
||||
void LlmChatBackend::setProviderResolver(ProviderResolver resolver)
|
||||
{
|
||||
m_providerResolver = std::move(resolver);
|
||||
}
|
||||
|
||||
LlmChatBackend::~LlmChatBackend()
|
||||
{
|
||||
cancel();
|
||||
@@ -42,7 +54,7 @@ void LlmChatBackend::sendTurn(const Session::TurnRequest &request)
|
||||
}
|
||||
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = Providers::ProvidersManager::instance().getProviderByName(providerName);
|
||||
auto *provider = m_providerResolver(providerName);
|
||||
|
||||
if (!provider) {
|
||||
const QString error = tr("No provider found with name: %1").arg(providerName);
|
||||
@@ -62,7 +74,7 @@ void LlmChatBackend::sendTurn(const Session::TurnRequest &request)
|
||||
}
|
||||
|
||||
LLMCore::ContextData context;
|
||||
if (request.context)
|
||||
if (request.context && Settings::chatAssistantSettings().useSystemPrompt())
|
||||
context.systemPrompt = Session::renderSystemPrompt(*request.context);
|
||||
context.history = renderHistory(*request.history, provider, promptTemplate);
|
||||
|
||||
@@ -73,8 +85,8 @@ void LlmChatBackend::sendTurn(const Session::TurnRequest &request)
|
||||
promptTemplate,
|
||||
context,
|
||||
LLMCore::RequestType::Chat,
|
||||
request.options.useTools,
|
||||
request.options.useThinking);
|
||||
Settings::chatAssistantSettings().enableChatTools(),
|
||||
Settings::chatAssistantSettings().enableThinkingMode());
|
||||
|
||||
provider->client()->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||
provider->client()->setTransferTimeout(
|
||||
@@ -88,10 +100,10 @@ void LlmChatBackend::sendTurn(const Session::TurnRequest &request)
|
||||
|
||||
m_provider = provider;
|
||||
m_dropPreToolText = !promptTemplate->supportsToolHistory();
|
||||
m_requestId
|
||||
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||
const QString requestId = m_ledger.beginTurn(
|
||||
provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint));
|
||||
|
||||
emit sessionEvent(Session::TurnStarted{.turnId = m_requestId});
|
||||
emit sessionEvent(Session::TurnStarted{.turnId = requestId});
|
||||
|
||||
bindToolSessions(provider);
|
||||
}
|
||||
@@ -102,7 +114,7 @@ void LlmChatBackend::cancel()
|
||||
return;
|
||||
|
||||
auto *provider = m_provider;
|
||||
const QString requestId = m_requestId;
|
||||
const QString requestId = m_ledger.activeTurnId();
|
||||
|
||||
releaseRequest();
|
||||
|
||||
@@ -114,14 +126,23 @@ void LlmChatBackend::cancel()
|
||||
|
||||
void LlmChatBackend::releaseRequest()
|
||||
{
|
||||
if (m_provider)
|
||||
cancelPendingPermissions();
|
||||
|
||||
if (m_provider) {
|
||||
disconnect(m_provider->client(), nullptr, this, nullptr);
|
||||
if (m_provider->toolsManager())
|
||||
m_provider->toolsManager()->setExecutionGate({});
|
||||
}
|
||||
|
||||
m_provider = nullptr;
|
||||
m_requestId.clear();
|
||||
m_dropPreToolText = false;
|
||||
}
|
||||
|
||||
Session::TurnContextNeeds LlmChatBackend::contextNeeds() const
|
||||
{
|
||||
return {Settings::chatAssistantSettings().useSystemPrompt()};
|
||||
}
|
||||
|
||||
void LlmChatBackend::setChatFilePath(const QString &filePath)
|
||||
{
|
||||
m_chatFilePath = filePath;
|
||||
@@ -133,7 +154,7 @@ void LlmChatBackend::clearToolSession(const QString &filePath)
|
||||
return;
|
||||
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = Providers::ProvidersManager::instance().getProviderByName(providerName);
|
||||
auto *provider = m_providerResolver(providerName);
|
||||
|
||||
if (!provider || !provider->capabilities().testFlag(Providers::ProviderCapability::Tools)
|
||||
|| !provider->toolsManager()) {
|
||||
@@ -209,6 +230,106 @@ void LlmChatBackend::bindToolSessions(Providers::Provider *provider)
|
||||
provider->toolsManager()->tool("read_original_history"))) {
|
||||
historyTool->setCurrentSessionId(m_chatFilePath);
|
||||
}
|
||||
|
||||
installExecutionGate(provider);
|
||||
}
|
||||
|
||||
void LlmChatBackend::installExecutionGate(Providers::Provider *provider)
|
||||
{
|
||||
provider->toolsManager()->setExecutionGate(
|
||||
[this](
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &input) {
|
||||
return gateToolExecution(requestId, toolId, toolName, input);
|
||||
});
|
||||
}
|
||||
|
||||
QFuture<bool> LlmChatBackend::gateToolExecution(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &input)
|
||||
{
|
||||
const auto allow = [] {
|
||||
QPromise<bool> promise;
|
||||
promise.start();
|
||||
promise.addResult(true);
|
||||
promise.finish();
|
||||
return promise.future();
|
||||
};
|
||||
|
||||
if (!m_ledger.isActiveTurn(requestId))
|
||||
return allow();
|
||||
|
||||
if (!m_provider || !m_provider->toolsManager())
|
||||
return allow();
|
||||
|
||||
auto *tool = m_provider->toolsManager()->tool(toolName);
|
||||
if (tool && tool->safety() == ::LLMQore::ToolSafety::ReadOnly)
|
||||
return allow();
|
||||
|
||||
auto promise = std::make_shared<QPromise<bool>>();
|
||||
promise->start();
|
||||
|
||||
const QString permissionId = m_ledger.registerPermission(
|
||||
[promise](const QString &optionId) {
|
||||
const bool allowed = optionId == Session::PermissionOptionKind::AllowOnce
|
||||
|| optionId == Session::PermissionOptionKind::AllowAlways;
|
||||
promise->addResult(allowed);
|
||||
promise->finish();
|
||||
},
|
||||
[promise] {
|
||||
promise->addResult(false);
|
||||
promise->finish();
|
||||
});
|
||||
|
||||
QFuture<bool> decision = promise->future();
|
||||
|
||||
const QString title = tool && !tool->displayName().isEmpty()
|
||||
? tr("Run %1").arg(tool->displayName())
|
||||
: tr("Run %1").arg(toolName);
|
||||
|
||||
emit sessionEvent(
|
||||
Session::PermissionRequested{
|
||||
.turnId = requestId,
|
||||
.requestId = permissionId,
|
||||
.toolCallId = toolId,
|
||||
.title = title,
|
||||
.toolKind = toolName,
|
||||
.options
|
||||
= {Session::PermissionOption{"allow_once", tr("Allow"), "allow_once"},
|
||||
Session::PermissionOption{
|
||||
"allow_always", tr("Allow for this conversation"), "allow_always"},
|
||||
Session::PermissionOption{"reject_once", tr("Don't run it"), "reject_once"}}});
|
||||
|
||||
Q_UNUSED(input)
|
||||
return decision;
|
||||
}
|
||||
|
||||
bool LlmChatBackend::respondPermission(const QString &requestId, const QString &optionId)
|
||||
{
|
||||
if (!m_ledger.resolvePermission(requestId, optionId))
|
||||
return false;
|
||||
|
||||
emit sessionEvent(
|
||||
Session::PermissionResolved{
|
||||
.turnId = m_ledger.activeTurnId(), .requestId = requestId, .optionId = optionId});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void LlmChatBackend::cancelPendingPermissions()
|
||||
{
|
||||
const QString turnId = m_ledger.activeTurnId();
|
||||
const QStringList cancelled = m_ledger.endTurn();
|
||||
|
||||
for (const QString &requestId : cancelled) {
|
||||
emit sessionEvent(
|
||||
Session::PermissionResolved{
|
||||
.turnId = turnId, .requestId = requestId, .cancelled = true});
|
||||
}
|
||||
}
|
||||
|
||||
QVector<LLMCore::Message> LlmChatBackend::renderHistory(
|
||||
@@ -222,7 +343,10 @@ QVector<LLMCore::Message> LlmChatBackend::renderHistory(
|
||||
int toolCallMsgIdx = -1;
|
||||
|
||||
for (const Session::MessageRow &row : Session::projectToRows(history)) {
|
||||
if (row.kind == Session::RowKind::Tool) {
|
||||
const Session::RowTreatment treatment
|
||||
= Session::rowTreatmentFor(Session::RowAudience::Prompt, row.kind);
|
||||
|
||||
if (treatment == Session::RowTreatment::ToolExchange) {
|
||||
if (!toolHistory || row.toolName.isEmpty())
|
||||
continue;
|
||||
|
||||
@@ -250,29 +374,29 @@ QVector<LLMCore::Message> LlmChatBackend::renderHistory(
|
||||
|
||||
toolCallMsgIdx = -1;
|
||||
|
||||
if (row.kind == Session::RowKind::FileEdit)
|
||||
if (treatment == Session::RowTreatment::Omit)
|
||||
continue;
|
||||
|
||||
LLMCore::Message apiMessage;
|
||||
apiMessage.role = row.kind == Session::RowKind::User ? "user" : "assistant";
|
||||
apiMessage.role = treatment == Session::RowTreatment::UserText ? "user" : "assistant";
|
||||
apiMessage.content = row.content;
|
||||
|
||||
if (!row.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
apiMessage.content += "\n\nAttached files:";
|
||||
for (const Session::AttachmentBlock &attachment : row.attachments) {
|
||||
const QString fileContent
|
||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.storedPath);
|
||||
const QByteArray fileContent = ChatFileStore::loadRawContentFromStorage(
|
||||
m_chatFilePath, attachment.storedPath);
|
||||
if (fileContent.isEmpty())
|
||||
continue;
|
||||
|
||||
const QString decodedContent = QString::fromUtf8(
|
||||
QByteArray::fromBase64(fileContent.toUtf8()));
|
||||
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
|
||||
.arg(attachment.fileName, decodedContent);
|
||||
apiMessage.content
|
||||
+= "\n\n"
|
||||
+ Session::fencedFileBlock(
|
||||
attachment.fileName, QString::fromUtf8(fileContent));
|
||||
}
|
||||
}
|
||||
|
||||
apiMessage.isThinking = row.kind == Session::RowKind::Thinking;
|
||||
apiMessage.isThinking = treatment == Session::RowTreatment::AssistantThinking;
|
||||
apiMessage.isRedacted = row.redacted;
|
||||
apiMessage.signature = row.signature;
|
||||
|
||||
@@ -296,7 +420,7 @@ QVector<LLMCore::ImageAttachment> LlmChatBackend::loadImagesFromStorage(
|
||||
|
||||
for (const Session::ImageBlock &storedImage : storedImages) {
|
||||
const QString base64Data
|
||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
||||
= ChatFileStore::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
||||
if (base64Data.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
||||
continue;
|
||||
@@ -315,7 +439,7 @@ QVector<LLMCore::ImageAttachment> LlmChatBackend::loadImagesFromStorage(
|
||||
|
||||
void LlmChatBackend::handleChunk(const QString &requestId, const QString &chunk)
|
||||
{
|
||||
if (requestId != m_requestId)
|
||||
if (!m_ledger.isActiveTurn(requestId))
|
||||
return;
|
||||
|
||||
emit sessionEvent(Session::TextDelta{.turnId = requestId, .text = chunk});
|
||||
@@ -323,7 +447,7 @@ void LlmChatBackend::handleChunk(const QString &requestId, const QString &chunk)
|
||||
|
||||
void LlmChatBackend::handleCompleted(const QString &requestId, const QString &fullText)
|
||||
{
|
||||
if (requestId != m_requestId)
|
||||
if (!m_ledger.isActiveTurn(requestId))
|
||||
return;
|
||||
|
||||
LOG_MESSAGE(
|
||||
@@ -337,7 +461,7 @@ void LlmChatBackend::handleCompleted(const QString &requestId, const QString &fu
|
||||
void LlmChatBackend::handleFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
||||
{
|
||||
if (requestId != m_requestId || !info.usage)
|
||||
if (!m_ledger.isActiveTurn(requestId) || !info.usage)
|
||||
return;
|
||||
|
||||
const auto &usage = *info.usage;
|
||||
@@ -361,7 +485,7 @@ void LlmChatBackend::handleFinalized(
|
||||
|
||||
void LlmChatBackend::handleFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
if (requestId != m_requestId)
|
||||
if (!m_ledger.isActiveTurn(requestId))
|
||||
return;
|
||||
|
||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
||||
@@ -374,7 +498,7 @@ void LlmChatBackend::handleFailed(const QString &requestId, const QString &error
|
||||
void LlmChatBackend::handleThinkingBlock(
|
||||
const QString &requestId, const QString &thinking, const QString &signature)
|
||||
{
|
||||
if (requestId != m_requestId)
|
||||
if (!m_ledger.isActiveTurn(requestId))
|
||||
return;
|
||||
|
||||
emit sessionEvent(
|
||||
@@ -391,14 +515,15 @@ void LlmChatBackend::handleToolStarted(
|
||||
const QString &toolName,
|
||||
const QJsonObject &arguments)
|
||||
{
|
||||
if (requestId != m_requestId)
|
||||
if (!m_ledger.isActiveTurn(requestId))
|
||||
return;
|
||||
|
||||
emit sessionEvent(
|
||||
Session::ToolCallStarted{
|
||||
Session::ToolCallUpdated{
|
||||
.turnId = requestId,
|
||||
.toolId = toolId,
|
||||
.name = toolName,
|
||||
.status = QStringLiteral("in_progress"),
|
||||
.arguments = arguments,
|
||||
.dropPrecedingText = m_dropPreToolText});
|
||||
}
|
||||
@@ -409,12 +534,17 @@ void LlmChatBackend::handleToolResult(
|
||||
const QString &toolName,
|
||||
const QString &toolOutput)
|
||||
{
|
||||
if (requestId != m_requestId)
|
||||
if (!m_ledger.isActiveTurn(requestId))
|
||||
return;
|
||||
|
||||
const bool failed = toolOutput.startsWith(QLatin1String("Error: "));
|
||||
emit sessionEvent(
|
||||
Session::ToolCallCompleted{
|
||||
.turnId = requestId, .toolId = toolId, .name = toolName, .result = toolOutput});
|
||||
Session::ToolCallUpdated{
|
||||
.turnId = requestId,
|
||||
.toolId = toolId,
|
||||
.name = toolName,
|
||||
.status = failed ? QStringLiteral("failed") : QStringLiteral("completed"),
|
||||
.result = toolOutput});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
Reference in New Issue
Block a user