mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-14 02:09:22 -04:00
refactor: IProjectScanner port; ContextManager QtC-free
This commit is contained in:
@@ -1103,11 +1103,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
||||||
{
|
{
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
|
||||||
if (project
|
|
||||||
&& m_clientInterface->contextManager()
|
|
||||||
->ignoreManager()
|
|
||||||
->shouldIgnore(filePath.toFSPathString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
||||||
.arg(filePath.toFSPathString()));
|
.arg(filePath.toFSPathString()));
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -273,9 +273,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
if (m_llmClient->contextManager()
|
if (m_llmClient->contextManager()->shouldIgnore(
|
||||||
->ignoreManager()
|
editor->textDocument()->filePath().toUrlishString())) {
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||||
return;
|
return;
|
||||||
@@ -319,9 +318,8 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (m_llmClient->contextManager()
|
if (m_llmClient->contextManager()->shouldIgnore(
|
||||||
->ignoreManager()
|
editor->textDocument()->filePath().toUrlishString())) {
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ add_library(Context STATIC
|
|||||||
DocumentContextReader.hpp DocumentContextReader.cpp
|
DocumentContextReader.hpp DocumentContextReader.cpp
|
||||||
ChangesManager.h ChangesManager.cpp
|
ChangesManager.h ChangesManager.cpp
|
||||||
ContextManager.hpp ContextManager.cpp
|
ContextManager.hpp ContextManager.cpp
|
||||||
|
IProjectScanner.hpp
|
||||||
|
ProjectScannerQtCreator.hpp ProjectScannerQtCreator.cpp
|
||||||
ContentFile.hpp
|
ContentFile.hpp
|
||||||
DocumentReaderQtCreator.hpp
|
DocumentReaderQtCreator.hpp
|
||||||
IDocumentReader.hpp
|
IDocumentReader.hpp
|
||||||
|
|||||||
@@ -6,25 +6,24 @@
|
|||||||
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
#include "settings/GeneralSettings.hpp"
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
|
||||||
#include <projectexplorer/project.h>
|
|
||||||
#include <projectexplorer/projectmanager.h>
|
|
||||||
#include <projectexplorer/projectnodes.h>
|
|
||||||
#include <texteditor/textdocument.h>
|
|
||||||
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
#include "ProjectScannerQtCreator.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
ContextManager::ContextManager(QObject *parent)
|
ContextManager::ContextManager(QObject *parent)
|
||||||
: QObject(parent)
|
: ContextManager(std::make_unique<ProjectScannerQtCreator>(), parent)
|
||||||
, m_ignoreManager(new IgnoreManager(this))
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
ContextManager::ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_scanner(std::move(scanner))
|
||||||
|
{}
|
||||||
|
|
||||||
|
ContextManager::~ContextManager() = default;
|
||||||
|
|
||||||
QString ContextManager::readFile(const QString &filePath) const
|
QString ContextManager::readFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
@@ -45,9 +44,7 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
|
|||||||
{
|
{
|
||||||
QList<ContentFile> files;
|
QList<ContentFile> files;
|
||||||
for (const QString &path : filePaths) {
|
for (const QString &path : filePaths) {
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(
|
if (m_scanner->shouldIgnore(path)) {
|
||||||
Utils::FilePath::fromString(path));
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(path, project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
|
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -58,27 +55,6 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
|
|
||||||
{
|
|
||||||
QStringList sourceFiles;
|
|
||||||
if (!project)
|
|
||||||
return sourceFiles;
|
|
||||||
|
|
||||||
auto projectNode = project->rootProjectNode();
|
|
||||||
if (!projectNode)
|
|
||||||
return sourceFiles;
|
|
||||||
|
|
||||||
projectNode->forEachNode(
|
|
||||||
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
|
|
||||||
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) {
|
|
||||||
sourceFiles.append(fileNode->filePath().toUrlishString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nullptr);
|
|
||||||
|
|
||||||
return sourceFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentFile ContextManager::createContentFile(const QString &filePath) const
|
ContentFile ContextManager::createContentFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
ContentFile contentFile;
|
ContentFile contentFile;
|
||||||
@@ -101,72 +77,25 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
|
|||||||
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
|
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(documentInfo)
|
Q_UNUSED(documentInfo)
|
||||||
// Language-specific completion presets were replaced by agent match rules.
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const
|
QString ContextManager::openedFilesContext(const QStringList &excludeFiles) const
|
||||||
{
|
|
||||||
auto documents = Core::DocumentModel::openedDocuments();
|
|
||||||
|
|
||||||
QList<QPair<QString, QString>> files;
|
|
||||||
|
|
||||||
for (const auto *document : std::as_const(documents)) {
|
|
||||||
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
|
|
||||||
if (!textDocument)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto filePath = textDocument->filePath().toUrlishString();
|
|
||||||
|
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!excludeFiles.contains(filePath)) {
|
|
||||||
files.append({filePath, textDocument->plainText()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
|
|
||||||
{
|
{
|
||||||
QString context = "User files context:\n";
|
QString context = "User files context:\n";
|
||||||
|
|
||||||
auto documents = Core::DocumentModel::openedDocuments();
|
for (const auto &file : m_scanner->openedTextFiles(excludeFiles)) {
|
||||||
|
context += QString("File: %1\n").arg(file.filePath);
|
||||||
for (const auto *document : documents) {
|
context += file.content;
|
||||||
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
|
|
||||||
if (!textDocument)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto filePath = textDocument->filePath().toUrlishString();
|
|
||||||
if (excludeFiles.contains(filePath))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
context += QString("File: %1\n").arg(filePath);
|
|
||||||
context += textDocument->plainText();
|
|
||||||
|
|
||||||
context += "\n";
|
context += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
IgnoreManager *ContextManager::ignoreManager() const
|
bool ContextManager::shouldIgnore(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return m_ignoreManager;
|
return m_scanner->shouldIgnore(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@@ -4,18 +4,16 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "ContentFile.hpp"
|
#include "ContentFile.hpp"
|
||||||
#include "IContextManager.hpp"
|
#include "IContextManager.hpp"
|
||||||
#include "IgnoreManager.hpp"
|
#include "IProjectScanner.hpp"
|
||||||
#include "ProgrammingLanguage.hpp"
|
#include "ProgrammingLanguage.hpp"
|
||||||
|
|
||||||
namespace ProjectExplorer {
|
|
||||||
class Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
class ContextManager : public QObject, public IContextManager
|
class ContextManager : public QObject, public IContextManager
|
||||||
@@ -24,22 +22,22 @@ class ContextManager : public QObject, public IContextManager
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ContextManager(QObject *parent = nullptr);
|
explicit ContextManager(QObject *parent = nullptr);
|
||||||
~ContextManager() override = default;
|
ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent = nullptr);
|
||||||
|
~ContextManager() override;
|
||||||
|
|
||||||
QString readFile(const QString &filePath) const override;
|
QString readFile(const QString &filePath) const override;
|
||||||
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
|
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
|
||||||
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
|
|
||||||
ContentFile createContentFile(const QString &filePath) const override;
|
ContentFile createContentFile(const QString &filePath) const override;
|
||||||
|
|
||||||
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
|
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
|
||||||
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
|
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
|
||||||
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
|
|
||||||
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
|
|
||||||
|
|
||||||
IgnoreManager *ignoreManager() const;
|
QString openedFilesContext(const QStringList &excludeFiles = QStringList{}) const;
|
||||||
|
|
||||||
|
bool shouldIgnore(const QString &filePath) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
IgnoreManager *m_ignoreManager;
|
std::unique_ptr<IProjectScanner> m_scanner;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@@ -11,10 +11,6 @@
|
|||||||
#include "IDocumentReader.hpp"
|
#include "IDocumentReader.hpp"
|
||||||
#include "ProgrammingLanguage.hpp"
|
#include "ProgrammingLanguage.hpp"
|
||||||
|
|
||||||
namespace ProjectExplorer {
|
|
||||||
class Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
class IContextManager
|
class IContextManager
|
||||||
@@ -24,7 +20,6 @@ public:
|
|||||||
|
|
||||||
virtual QString readFile(const QString &filePath) const = 0;
|
virtual QString readFile(const QString &filePath) const = 0;
|
||||||
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
|
virtual QList<ContentFile> getContentFiles(const QStringList &filePaths) const = 0;
|
||||||
virtual QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const = 0;
|
|
||||||
virtual ContentFile createContentFile(const QString &filePath) const = 0;
|
virtual ContentFile createContentFile(const QString &filePath) const = 0;
|
||||||
|
|
||||||
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;
|
virtual ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const = 0;
|
||||||
|
|||||||
28
context/IProjectScanner.hpp
Normal file
28
context/IProjectScanner.hpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
struct OpenedTextFile
|
||||||
|
{
|
||||||
|
QString filePath;
|
||||||
|
QString content;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IProjectScanner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IProjectScanner() = default;
|
||||||
|
|
||||||
|
virtual QList<OpenedTextFile> openedTextFiles(const QStringList &excludeFiles = {}) const = 0;
|
||||||
|
virtual bool shouldIgnore(const QString &filePath) const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
53
context/ProjectScannerQtCreator.cpp
Normal file
53
context/ProjectScannerQtCreator.cpp
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include "ProjectScannerQtCreator.hpp"
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <texteditor/textdocument.h>
|
||||||
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
|
#include "IgnoreManager.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
ProjectScannerQtCreator::ProjectScannerQtCreator()
|
||||||
|
: m_ignoreManager(std::make_unique<IgnoreManager>())
|
||||||
|
{}
|
||||||
|
|
||||||
|
ProjectScannerQtCreator::~ProjectScannerQtCreator() = default;
|
||||||
|
|
||||||
|
QList<OpenedTextFile> ProjectScannerQtCreator::openedTextFiles(
|
||||||
|
const QStringList &excludeFiles) const
|
||||||
|
{
|
||||||
|
QList<OpenedTextFile> files;
|
||||||
|
|
||||||
|
const auto documents = Core::DocumentModel::openedDocuments();
|
||||||
|
for (const auto *document : documents) {
|
||||||
|
const auto *textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
|
||||||
|
if (!textDocument)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const QString filePath = textDocument->filePath().toUrlishString();
|
||||||
|
if (excludeFiles.contains(filePath))
|
||||||
|
continue;
|
||||||
|
if (shouldIgnore(filePath))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
files.append({filePath, textDocument->plainText()});
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProjectScannerQtCreator::shouldIgnore(const QString &filePath) const
|
||||||
|
{
|
||||||
|
auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||||
|
Utils::FilePath::fromString(filePath));
|
||||||
|
return project && m_ignoreManager->shouldIgnore(filePath, project);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
28
context/ProjectScannerQtCreator.hpp
Normal file
28
context/ProjectScannerQtCreator.hpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "IProjectScanner.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
|
class IgnoreManager;
|
||||||
|
|
||||||
|
class ProjectScannerQtCreator : public IProjectScanner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ProjectScannerQtCreator();
|
||||||
|
~ProjectScannerQtCreator() override;
|
||||||
|
|
||||||
|
QList<OpenedTextFile> openedTextFiles(const QStringList &excludeFiles = {}) const override;
|
||||||
|
bool shouldIgnore(const QString &filePath) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<IgnoreManager> m_ignoreManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Context
|
||||||
@@ -4,6 +4,8 @@ add_executable(QodeAssistTest
|
|||||||
CodeHandlerTest.cpp
|
CodeHandlerTest.cpp
|
||||||
DocumentContextReaderTest.cpp
|
DocumentContextReaderTest.cpp
|
||||||
LLMSuggestionTest.cpp
|
LLMSuggestionTest.cpp
|
||||||
|
JsonPromptTemplateTest.cpp
|
||||||
|
ResponseRouterTest.cpp
|
||||||
# LLMClientInterfaceTests.cpp
|
# LLMClientInterfaceTests.cpp
|
||||||
unittest_main.cpp
|
unittest_main.cpp
|
||||||
)
|
)
|
||||||
@@ -18,6 +20,9 @@ target_link_libraries(QodeAssistTest PRIVATE
|
|||||||
Context
|
Context
|
||||||
Common
|
Common
|
||||||
LLMQore
|
LLMQore
|
||||||
|
Templates
|
||||||
|
Agents
|
||||||
|
Session
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistTest PRIVATE ${CMAKE_SOURCE_DIR})
|
target_include_directories(QodeAssistTest PRIVATE ${CMAKE_SOURCE_DIR})
|
||||||
|
|||||||
129
test/JsonPromptTemplateTest.cpp
Normal file
129
test/JsonPromptTemplateTest.cpp
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
#include <ContextData.hpp>
|
||||||
|
#include <JsonPromptTemplate.hpp>
|
||||||
|
|
||||||
|
using QodeAssist::AgentConfig;
|
||||||
|
using QodeAssist::Templates::ContextData;
|
||||||
|
using QodeAssist::Templates::JsonPromptTemplate;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
AgentConfig makeConfig(const QJsonObject &body)
|
||||||
|
{
|
||||||
|
AgentConfig cfg;
|
||||||
|
cfg.name = QStringLiteral("test-agent");
|
||||||
|
cfg.body = body;
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString kUserMessages
|
||||||
|
= QStringLiteral("[ { \"role\": \"user\", \"content\": {{ tojson(ctx.prefix) }} } ]");
|
||||||
|
|
||||||
|
const QString kSystemField = QStringLiteral(
|
||||||
|
"{% if existsIn(ctx, \"system_prompt\") %}{{ tojson(ctx.system_prompt) }}{% endif %}");
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(JsonPromptTemplateTest, RendersJinjaSplicesAndKeepsLiterals)
|
||||||
|
{
|
||||||
|
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||||
|
{"max_tokens", 128},
|
||||||
|
{"temperature", 0.5},
|
||||||
|
{"stream", true},
|
||||||
|
{"messages", kUserMessages},
|
||||||
|
}));
|
||||||
|
ASSERT_NE(tmpl, nullptr);
|
||||||
|
|
||||||
|
ContextData ctx;
|
||||||
|
ctx.prefix = QStringLiteral("hello world");
|
||||||
|
|
||||||
|
QJsonObject request;
|
||||||
|
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||||
|
|
||||||
|
EXPECT_EQ(request.value("max_tokens").toInt(), 128);
|
||||||
|
EXPECT_DOUBLE_EQ(request.value("temperature").toDouble(), 0.5);
|
||||||
|
EXPECT_TRUE(request.value("stream").toBool());
|
||||||
|
|
||||||
|
const QJsonArray messages = request.value("messages").toArray();
|
||||||
|
ASSERT_EQ(messages.size(), 1);
|
||||||
|
EXPECT_EQ(
|
||||||
|
messages.at(0).toObject().value("content").toString(), QStringLiteral("hello world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(JsonPromptTemplateTest, DropsKeyWhenJinjaRendersEmpty)
|
||||||
|
{
|
||||||
|
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||||
|
{"system", kSystemField},
|
||||||
|
{"messages", kUserMessages},
|
||||||
|
}));
|
||||||
|
ASSERT_NE(tmpl, nullptr);
|
||||||
|
|
||||||
|
ContextData ctx;
|
||||||
|
ctx.prefix = QStringLiteral("hi");
|
||||||
|
|
||||||
|
QJsonObject request;
|
||||||
|
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||||
|
|
||||||
|
EXPECT_FALSE(request.contains(QStringLiteral("system")));
|
||||||
|
EXPECT_TRUE(request.contains(QStringLiteral("messages")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(JsonPromptTemplateTest, RendersSystemPromptWhenPresent)
|
||||||
|
{
|
||||||
|
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||||
|
{"system", kSystemField},
|
||||||
|
{"messages", kUserMessages},
|
||||||
|
}));
|
||||||
|
ASSERT_NE(tmpl, nullptr);
|
||||||
|
|
||||||
|
ContextData ctx;
|
||||||
|
ctx.prefix = QStringLiteral("hi");
|
||||||
|
ctx.systemPrompt = QStringLiteral("You are a helpful assistant.");
|
||||||
|
|
||||||
|
QJsonObject request;
|
||||||
|
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
request.value("system").toString(), QStringLiteral("You are a helpful assistant."));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(JsonPromptTemplateTest, PreservesNestedLiteralObjects)
|
||||||
|
{
|
||||||
|
auto tmpl = JsonPromptTemplate::fromConfig(makeConfig(QJsonObject{
|
||||||
|
{"thinking", QJsonObject{{"type", "adaptive"}, {"budget", 8192}}},
|
||||||
|
{"messages", kUserMessages},
|
||||||
|
}));
|
||||||
|
ASSERT_NE(tmpl, nullptr);
|
||||||
|
|
||||||
|
ContextData ctx;
|
||||||
|
ctx.prefix = QStringLiteral("x");
|
||||||
|
|
||||||
|
QJsonObject request;
|
||||||
|
ASSERT_TRUE(tmpl->buildFullRequest(request, ctx));
|
||||||
|
|
||||||
|
const QJsonObject thinking = request.value("thinking").toObject();
|
||||||
|
EXPECT_EQ(thinking.value("type").toString(), QStringLiteral("adaptive"));
|
||||||
|
EXPECT_EQ(thinking.value("budget").toInt(), 8192);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(JsonPromptTemplateTest, RejectsBodyThatRendersInvalidJsonAtLoad)
|
||||||
|
{
|
||||||
|
QString error;
|
||||||
|
auto tmpl = JsonPromptTemplate::fromConfig(
|
||||||
|
makeConfig(QJsonObject{
|
||||||
|
{"messages", QStringLiteral("[ {{ tojson(ctx.prefix) }}")},
|
||||||
|
}),
|
||||||
|
&error);
|
||||||
|
|
||||||
|
EXPECT_EQ(tmpl, nullptr);
|
||||||
|
EXPECT_FALSE(error.isEmpty());
|
||||||
|
}
|
||||||
159
test/ResponseRouterTest.cpp
Normal file
159
test/ResponseRouterTest.cpp
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QFuture>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <LLMQore/ToolResult.hpp>
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <ErrorInfo.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <ResponseEvent.hpp>
|
||||||
|
#include <ResponseRouter.hpp>
|
||||||
|
|
||||||
|
using namespace QodeAssist;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class FakeClient : public LLMQore::BaseClient
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using LLMQore::BaseClient::BaseClient;
|
||||||
|
|
||||||
|
void fireChunk(const QString &id, const QString &chunk) { emit chunkReceived(id, chunk); }
|
||||||
|
void fireThinking(const QString &id, const QString &thinking, const QString &signature)
|
||||||
|
{
|
||||||
|
emit thinkingBlockReceived(id, thinking, signature);
|
||||||
|
}
|
||||||
|
void fireToolStarted(
|
||||||
|
const QString &id, const QString &toolId, const QString &name, const QJsonObject &args)
|
||||||
|
{
|
||||||
|
emit toolStarted(id, toolId, name, args);
|
||||||
|
}
|
||||||
|
void fireToolResult(
|
||||||
|
const QString &id, const QString &toolId, const QString &name, const QString &result)
|
||||||
|
{
|
||||||
|
emit toolResultReady(id, toolId, name, result);
|
||||||
|
}
|
||||||
|
void fireFinalized(const QString &id, const LLMQore::CompletionInfo &info)
|
||||||
|
{
|
||||||
|
emit requestFinalized(id, info);
|
||||||
|
}
|
||||||
|
void fireFailed(const QString &id, const QString &error) { emit requestFailed(id, error); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LLMQore::RequestID sendMessage(
|
||||||
|
const QJsonObject &, const QString &, LLMQore::RequestMode) override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
LLMQore::RequestID ask(const QString &, LLMQore::RequestMode) override { return {}; }
|
||||||
|
QFuture<QList<QString>> listModels() override { return {}; }
|
||||||
|
LLMQore::ToolSchemaFormat toolSchemaFormat() const override
|
||||||
|
{
|
||||||
|
return LLMQore::ToolSchemaFormat::Claude;
|
||||||
|
}
|
||||||
|
void processData(const LLMQore::RequestID &, const QByteArray &) override {}
|
||||||
|
void processBufferedResponse(const LLMQore::RequestID &, const QByteArray &) override {}
|
||||||
|
QNetworkRequest prepareNetworkRequest(const QUrl &) const override { return {}; }
|
||||||
|
LLMQore::BaseMessage *messageForRequest(const LLMQore::RequestID &) const override
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
void cleanupDerivedData(const LLMQore::RequestID &) override {}
|
||||||
|
QJsonObject buildContinuationPayload(
|
||||||
|
const QJsonObject &,
|
||||||
|
LLMQore::BaseMessage *,
|
||||||
|
const QHash<QString, LLMQore::ToolResult> &) override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(ResponseRouterTest, BuildsAssistantTurnAndEmitsEvents)
|
||||||
|
{
|
||||||
|
FakeClient client;
|
||||||
|
ConversationHistory history;
|
||||||
|
ResponseRouter router(&client, &history);
|
||||||
|
|
||||||
|
QVector<ResponseEvent::Kind> kinds;
|
||||||
|
QObject::connect(&router, &ResponseRouter::event, &router, [&kinds](const ResponseEvent &ev) {
|
||||||
|
kinds.append(ev.kind());
|
||||||
|
});
|
||||||
|
|
||||||
|
const QString id = QStringLiteral("req-1");
|
||||||
|
router.beginRequest(id);
|
||||||
|
client.fireThinking(id, QStringLiteral("pondering"), QStringLiteral("sig"));
|
||||||
|
client.fireChunk(id, QStringLiteral("Hello"));
|
||||||
|
client.fireChunk(id, QStringLiteral(" world"));
|
||||||
|
client.fireToolStarted(
|
||||||
|
id, QStringLiteral("t1"), QStringLiteral("read_file"), QJsonObject{{"path", "a.txt"}});
|
||||||
|
client.fireToolResult(
|
||||||
|
id, QStringLiteral("t1"), QStringLiteral("read_file"), QStringLiteral("contents"));
|
||||||
|
|
||||||
|
LLMQore::CompletionInfo info;
|
||||||
|
info.stopReason = QStringLiteral("end_turn");
|
||||||
|
info.usage = LLMQore::TokenUsage{12, 34, 0, 0};
|
||||||
|
client.fireFinalized(id, info);
|
||||||
|
|
||||||
|
ASSERT_EQ(history.size(), 2);
|
||||||
|
|
||||||
|
const Message &assistant = history.messages()[0];
|
||||||
|
EXPECT_EQ(assistant.role(), Message::Role::Assistant);
|
||||||
|
EXPECT_EQ(assistant.id(), id);
|
||||||
|
EXPECT_EQ(assistant.text(), QStringLiteral("Hello world"));
|
||||||
|
EXPECT_TRUE(assistant.hasToolUse());
|
||||||
|
|
||||||
|
const Message &toolResult = history.messages()[1];
|
||||||
|
EXPECT_EQ(toolResult.role(), Message::Role::User);
|
||||||
|
|
||||||
|
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::ThinkingDelta));
|
||||||
|
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::TextDelta));
|
||||||
|
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::ToolResult));
|
||||||
|
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::Usage));
|
||||||
|
EXPECT_TRUE(kinds.contains(ResponseEvent::Kind::MessageStop));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ResponseRouterTest, CategorizesAuthError)
|
||||||
|
{
|
||||||
|
FakeClient client;
|
||||||
|
ConversationHistory history;
|
||||||
|
ResponseRouter router(&client, &history);
|
||||||
|
|
||||||
|
std::optional<ResponseEvents::Error> captured;
|
||||||
|
QObject::connect(&router, &ResponseRouter::event, &router, [&captured](const ResponseEvent &ev) {
|
||||||
|
if (ev.kind() == ResponseEvent::Kind::Error)
|
||||||
|
captured = *ev.as<ResponseEvents::Error>();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.beginRequest(QStringLiteral("req-2"));
|
||||||
|
client.fireFailed(
|
||||||
|
QStringLiteral("req-2"), QStringLiteral("HTTP 401 Unauthorized: invalid api key"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(captured.has_value());
|
||||||
|
EXPECT_EQ(captured->category, ErrorCategory::Auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ResponseRouterTest, IgnoresEventsForInactiveRequest)
|
||||||
|
{
|
||||||
|
FakeClient client;
|
||||||
|
ConversationHistory history;
|
||||||
|
ResponseRouter router(&client, &history);
|
||||||
|
|
||||||
|
router.beginRequest(QStringLiteral("req-3"));
|
||||||
|
client.fireChunk(QStringLiteral("OTHER"), QStringLiteral("ignored"));
|
||||||
|
|
||||||
|
EXPECT_TRUE(history.isEmpty());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user