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:
@@ -8,15 +8,19 @@
|
||||
#include <QFileInfo>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include "ChatHistoryBridge.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <utils/filepath.h>
|
||||
|
||||
#include "ChatFileStore.hpp"
|
||||
#include "LlmChatBackend.hpp"
|
||||
#include "TurnContextAdapters.hpp"
|
||||
#include "context/ChangesManager.h"
|
||||
#include "context/RulesLoader.hpp"
|
||||
#include "context/FileEditManager.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "session/FileEditPayload.hpp"
|
||||
#include "session/TurnContextBuilder.hpp"
|
||||
#include "acp/AcpChatBackend.hpp"
|
||||
#include "mcp/AgentKnowledgeServer.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
@@ -28,9 +32,42 @@ ChatController::ChatController(
|
||||
, m_chatModel(chatModel)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
, m_session(new Session::Session(this))
|
||||
, m_backend(new LlmChatBackend(promptProvider, this))
|
||||
, m_llmBackend(new LlmChatBackend(promptProvider, this))
|
||||
, m_acpBackend(new Acp::AcpChatBackend(this))
|
||||
, m_agentKnowledge(new Mcp::AgentKnowledgeServer(this))
|
||||
, m_backend(m_llmBackend)
|
||||
{
|
||||
new ChatHistoryBridge(m_session, chatModel, this);
|
||||
connect(m_session, &Session::Session::rowsReset, m_chatModel, &ChatModel::resetMessages);
|
||||
connect(m_session, &Session::Session::rowsAppended, m_chatModel, &ChatModel::appendMessages);
|
||||
connect(m_session, &Session::Session::rowUpdated, m_chatModel, &ChatModel::updateMessage);
|
||||
connect(m_session, &Session::Session::rowsRemoved, m_chatModel, &ChatModel::removeMessages);
|
||||
m_chatModel->resetMessages(m_session->rows());
|
||||
|
||||
m_acpBackend->setStoredContentLoader(&ChatFileStore::loadRawContentFromStorage);
|
||||
m_agentKnowledge->setIgnorePredicate([this](const QString &filePath) {
|
||||
auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||
Utils::FilePath::fromString(filePath));
|
||||
return m_contextManager->ignoreManager()->shouldIgnore(filePath, project);
|
||||
});
|
||||
m_acpBackend->setKnowledgeService(m_agentKnowledge);
|
||||
|
||||
connect(
|
||||
m_session,
|
||||
&Session::Session::sessionInfoReceived,
|
||||
this,
|
||||
&ChatController::sessionInfoReceived);
|
||||
|
||||
connect(
|
||||
m_acpBackend,
|
||||
&Acp::AcpChatBackend::agentSessionUnavailable,
|
||||
this,
|
||||
&ChatController::agentSessionUnavailable);
|
||||
|
||||
connect(
|
||||
m_acpBackend,
|
||||
&Acp::AcpChatBackend::availableCommandsChanged,
|
||||
this,
|
||||
&ChatController::agentCommandsChanged);
|
||||
|
||||
m_session->setBackend(m_backend);
|
||||
|
||||
@@ -39,7 +76,7 @@ ChatController::ChatController(
|
||||
|
||||
connect(m_session, &Session::Session::turnFinished, this, [this](const QString &turnId) {
|
||||
QString applyError;
|
||||
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(turnId, &applyError)) {
|
||||
if (!Context::FileEditManager::instance().applyPendingEditsForRequest(turnId, &applyError)) {
|
||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
||||
.arg(turnId, applyError));
|
||||
}
|
||||
@@ -56,14 +93,53 @@ ChatController::ChatController(
|
||||
|
||||
connect(m_session, &Session::Session::rowsReset, this, [this] { registerHistoricalEdits(); });
|
||||
|
||||
auto &changes = Context::ChangesManager::instance();
|
||||
connect(&changes, &Context::ChangesManager::fileEditApplied, this, [this](const QString &id) {
|
||||
connect(
|
||||
m_session,
|
||||
&Session::Session::agentFileEditRecorded,
|
||||
this,
|
||||
[](const QString &turnId,
|
||||
const QString &editId,
|
||||
const QString &filePath,
|
||||
const QString &oldContent,
|
||||
const QString &newContent) {
|
||||
const QFileInfo info(filePath);
|
||||
const QString canonical = info.canonicalFilePath();
|
||||
const Utils::FilePath target = Utils::FilePath::fromString(
|
||||
canonical.isEmpty() ? info.absoluteFilePath() : canonical);
|
||||
|
||||
bool insideProject = false;
|
||||
const QList<ProjectExplorer::Project *> projects
|
||||
= ProjectExplorer::ProjectManager::projects();
|
||||
for (const ProjectExplorer::Project *project : projects) {
|
||||
if (target.isChildOf(project->projectDirectory())) {
|
||||
insideProject = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!insideProject) {
|
||||
LOG_MESSAGE(
|
||||
QString("Agent edit %1 targets %2 outside every open project; recorded "
|
||||
"in the transcript only, without apply/undo actions")
|
||||
.arg(editId, filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
Context::FileEditManager::instance().registerAppliedFileEdit(
|
||||
editId, target.toUrlishString(), oldContent, newContent, turnId);
|
||||
});
|
||||
|
||||
auto &changes = Context::FileEditManager::instance();
|
||||
connect(&changes, &Context::FileEditManager::fileEditApplied, this, [this](const QString &id) {
|
||||
m_session->updateFileEditStatus(id, "applied", "Successfully applied");
|
||||
});
|
||||
connect(&changes, &Context::ChangesManager::fileEditRejected, this, [this](const QString &id) {
|
||||
connect(&changes, &Context::FileEditManager::fileEditRejected, this, [this](const QString &id) {
|
||||
m_session->updateFileEditStatus(id, "rejected", "Rejected by user");
|
||||
});
|
||||
connect(&changes, &Context::ChangesManager::fileEditArchived, this, [this](const QString &id) {
|
||||
connect(&changes, &Context::FileEditManager::fileEditUndone, this, [this](const QString &id) {
|
||||
recordFileEditStatus(id, "rejected", "Successfully undone");
|
||||
});
|
||||
connect(&changes, &Context::FileEditManager::fileEditArchived, this, [this](const QString &id) {
|
||||
m_session->updateFileEditStatus(id, "archived", "Archived (from previous conversation turn)");
|
||||
});
|
||||
}
|
||||
@@ -73,32 +149,107 @@ void ChatController::setSkillsManager(Skills::SkillsManager *skillsManager)
|
||||
m_skillsManager = skillsManager;
|
||||
}
|
||||
|
||||
void ChatController::bindAgent(const Acp::AgentDefinition &agent)
|
||||
{
|
||||
m_acpBackend->bindAgent(agent);
|
||||
activateBackend(m_acpBackend);
|
||||
}
|
||||
|
||||
void ChatController::bindLlm()
|
||||
{
|
||||
activateBackend(m_llmBackend);
|
||||
}
|
||||
|
||||
QString ChatController::boundAgentId() const
|
||||
{
|
||||
return m_backend == m_acpBackend ? m_acpBackend->boundAgentId() : QString();
|
||||
}
|
||||
|
||||
QString ChatController::boundAgentName() const
|
||||
{
|
||||
return m_backend == m_acpBackend ? m_acpBackend->boundAgentName() : QString();
|
||||
}
|
||||
|
||||
QList<LLMQore::Acp::AvailableCommand> ChatController::agentCommands() const
|
||||
{
|
||||
if (m_backend != m_acpBackend)
|
||||
return {};
|
||||
return m_acpBackend->availableCommands();
|
||||
}
|
||||
|
||||
bool ChatController::transcriptEmpty() const
|
||||
{
|
||||
return m_session->rows().isEmpty();
|
||||
}
|
||||
|
||||
Acp::AgentBinding ChatController::agentBinding() const
|
||||
{
|
||||
if (m_backend != m_acpBackend)
|
||||
return {};
|
||||
|
||||
return Acp::AgentBinding{m_acpBackend->boundAgentId(), m_acpBackend->bindingSessionId()};
|
||||
}
|
||||
|
||||
void ChatController::resumeAgentSession(const QString &sessionId)
|
||||
{
|
||||
m_acpBackend->resumeSession(sessionId);
|
||||
}
|
||||
|
||||
void ChatController::startFreshAgentSession()
|
||||
{
|
||||
m_acpBackend->startFreshSession();
|
||||
}
|
||||
|
||||
void ChatController::startFreshAgentSession(const QString &handoverSummary)
|
||||
{
|
||||
m_acpBackend->clearToolSession(m_chatFilePath);
|
||||
m_acpBackend->startFreshSession();
|
||||
m_acpBackend->setHandoverSummary(handoverSummary);
|
||||
}
|
||||
|
||||
void ChatController::releaseAgentSession()
|
||||
{
|
||||
m_acpBackend->clearToolSession(m_chatFilePath);
|
||||
}
|
||||
|
||||
bool ChatController::conversationStarted() const
|
||||
{
|
||||
return !m_session->history().messages().isEmpty();
|
||||
}
|
||||
|
||||
void ChatController::activateBackend(Session::ChatBackend *backend)
|
||||
{
|
||||
if (m_backend == backend)
|
||||
return;
|
||||
|
||||
m_session->cancel();
|
||||
|
||||
if (m_backend)
|
||||
m_backend->clearToolSession(m_chatFilePath);
|
||||
|
||||
m_backend = backend;
|
||||
m_session->setBackend(backend);
|
||||
backend->setChatFilePath(m_chatFilePath);
|
||||
}
|
||||
|
||||
Session::Session *ChatController::session() const
|
||||
{
|
||||
return m_session;
|
||||
}
|
||||
|
||||
void ChatController::sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments,
|
||||
const QList<QString> &linkedFiles,
|
||||
bool useTools,
|
||||
bool useThinking)
|
||||
void ChatController::sendMessage(const QString &message, const QList<QString> &attachments)
|
||||
{
|
||||
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
|
||||
LOG_MESSAGE("Ignoring empty chat message");
|
||||
return;
|
||||
}
|
||||
|
||||
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||
Context::FileEditManager::instance().archiveAllNonArchivedEdits();
|
||||
|
||||
m_session->sendTurn(
|
||||
composeUserBlocks(message, attachments),
|
||||
buildTurnContext(message, linkedFiles),
|
||||
Session::TurnOptions{useTools, useThinking});
|
||||
m_session->sendTurn(composeUserBlocks(message, attachments), buildTurnContext(message));
|
||||
}
|
||||
|
||||
void ChatController::clearMessages()
|
||||
void ChatController::clearConversation()
|
||||
{
|
||||
m_backend->clearToolSession(m_chatFilePath);
|
||||
m_session->clear();
|
||||
@@ -114,6 +265,11 @@ void ChatController::resetToRow(int rowIndex)
|
||||
m_session->truncateRows(rowIndex);
|
||||
}
|
||||
|
||||
void ChatController::respondToPermission(const QString &requestId, const QString &optionId)
|
||||
{
|
||||
m_session->respondPermission(requestId, optionId);
|
||||
}
|
||||
|
||||
QList<Session::ContentBlock> ChatController::composeUserBlocks(
|
||||
const QString &message, const QList<QString> &attachments)
|
||||
{
|
||||
@@ -132,7 +288,7 @@ QList<Session::ContentBlock> ChatController::composeUserBlocks(
|
||||
if (!textFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
for (const auto &file : m_contextManager->getContentFiles(textFiles)) {
|
||||
QString storedPath;
|
||||
if (!ChatSerializer::saveContentToStorage(
|
||||
if (!ChatFileStore::saveContentToStorage(
|
||||
m_chatFilePath, file.filename, file.content.toUtf8().toBase64(), storedPath)) {
|
||||
continue;
|
||||
}
|
||||
@@ -152,7 +308,7 @@ QList<Session::ContentBlock> ChatController::composeUserBlocks(
|
||||
|
||||
const QFileInfo fileInfo(imagePath);
|
||||
QString storedPath;
|
||||
if (!ChatSerializer::saveContentToStorage(
|
||||
if (!ChatFileStore::saveContentToStorage(
|
||||
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
||||
continue;
|
||||
}
|
||||
@@ -169,32 +325,23 @@ QList<Session::ContentBlock> ChatController::composeUserBlocks(
|
||||
return blocks;
|
||||
}
|
||||
|
||||
std::optional<Session::TurnContext> ChatController::buildTurnContext(
|
||||
const QString &message, const QList<QString> &linkedFiles) const
|
||||
Session::TurnContext ChatController::buildTurnContext(const QString &message) const
|
||||
{
|
||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||
if (!chatAssistantSettings.useSystemPrompt())
|
||||
return std::nullopt;
|
||||
|
||||
Session::TurnContextRequest contextRequest;
|
||||
contextRequest.message = message;
|
||||
contextRequest.basePrompt = chatAssistantSettings.systemPrompt();
|
||||
contextRequest.linkedFilePaths = linkedFiles;
|
||||
contextRequest.needs = m_backend->contextNeeds();
|
||||
|
||||
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
|
||||
if (!lastRoleId.isEmpty()) {
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (!role.id.isEmpty())
|
||||
contextRequest.rolePrompt = role.systemPrompt;
|
||||
}
|
||||
if (contextRequest.needs.systemPrompt)
|
||||
contextRequest.basePrompt = chatAssistantSettings.systemPrompt();
|
||||
|
||||
auto *project = Context::RulesLoader::getActiveProject();
|
||||
auto *project = activeProject();
|
||||
|
||||
ProjectContextQtCreator projectPort(project);
|
||||
LinkedFilesQtCreator linkedFilesPort(m_contextManager);
|
||||
auto skillsPort = makeSkillsContext(m_skillsManager, project);
|
||||
|
||||
const Session::TurnContextBuilder builder(projectPort, skillsPort.get(), linkedFilesPort);
|
||||
const Session::TurnContextBuilder builder(projectPort, skillsPort.get());
|
||||
|
||||
return builder.build(contextRequest);
|
||||
}
|
||||
@@ -202,7 +349,7 @@ std::optional<Session::TurnContext> ChatController::buildTurnContext(
|
||||
void ChatController::recordFileEditStatus(
|
||||
const QString &editId, const QString &status, const QString &fallbackMessage)
|
||||
{
|
||||
const auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
const auto edit = Context::FileEditManager::instance().getFileEdit(editId);
|
||||
const QString message = edit.statusMessage.isEmpty() ? fallbackMessage : edit.statusMessage;
|
||||
m_session->updateFileEditStatus(editId, status, message);
|
||||
}
|
||||
@@ -232,7 +379,7 @@ void ChatController::registerHistoricalEdits()
|
||||
continue;
|
||||
}
|
||||
|
||||
Context::ChangesManager::instance().addFileEdit(
|
||||
Context::FileEditManager::instance().addFileEdit(
|
||||
editId,
|
||||
filePath,
|
||||
payload->value("old_content").toString(),
|
||||
|
||||
Reference in New Issue
Block a user