From 3bf99327b82167f8ce19d10788ccf02e6b8fb14c Mon Sep 17 00:00:00 2001 From: Povilas Kanapickas Date: Mon, 10 Mar 2025 03:30:37 +0200 Subject: [PATCH 1/2] chore: Add tests for LLMClientInterface --- CMakeLists.txt | 2 +- LLMClientInterface.hpp | 4 +- test/CMakeLists.txt | 6 + test/LLMClientInterfaceTests.cpp | 336 +++++++++++++++++++++++++++++++ test/MockDocumentReader.hpp | 53 +++++ test/MockRequestHandler.hpp | 59 ++++++ 6 files changed, 458 insertions(+), 2 deletions(-) create mode 100644 test/LLMClientInterfaceTests.cpp create mode 100644 test/MockDocumentReader.hpp create mode 100644 test/MockRequestHandler.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 248cd47..661f9c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_POSITION_INDEPENDENT_CODE ON) find_package(QtCreator REQUIRED COMPONENTS Core) -find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED) +find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test REQUIRED) find_package(GTest) # IDE_VERSION is defined by QtCreator package diff --git a/LLMClientInterface.hpp b/LLMClientInterface.hpp index d0a4971..122c4ed 100644 --- a/LLMClientInterface.hpp +++ b/LLMClientInterface.hpp @@ -58,9 +58,11 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface void handleCompletion(const QJsonObject &request); + // exposed for tests + void sendData(const QByteArray &data) override; + protected: void startImpl() override; - void sendData(const QByteArray &data) override; void parseCurrentMessage() override; private: diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e6291d8..1dbeaf7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,16 +1,22 @@ add_executable(QodeAssistTest ../CodeHandler.cpp + ../LLMClientInterface.cpp CodeHandlerTest.cpp DocumentContextReaderTest.cpp + LLMClientInterfaceTests.cpp unittest_main.cpp ) target_link_libraries(QodeAssistTest PRIVATE Qt::Core + Qt::Test GTest::GTest + GTest::gmock GTest::Main QtCreator::LanguageClient Context ) +target_compile_definitions(QodeAssistTest PRIVATE CMAKE_CURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") + add_test(NAME QodeAssistTest COMMAND QodeAssistTest) diff --git a/test/LLMClientInterfaceTests.cpp b/test/LLMClientInterfaceTests.cpp new file mode 100644 index 0000000..94efbe9 --- /dev/null +++ b/test/LLMClientInterfaceTests.cpp @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2025 Povilas Kanapickas + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include +#include + +#include +#include +#include +#include + +#include "LLMClientInterface.hpp" +#include "MockDocumentReader.hpp" +#include "MockRequestHandler.hpp" +#include "llmcore/IPromptProvider.hpp" +#include "llmcore/IProviderRegistry.hpp" +#include "logger/EmptyRequestPerformanceLogger.hpp" +#include "settings/CodeCompletionSettings.hpp" +#include "settings/GeneralSettings.hpp" +#include "templates/Templates.hpp" +#include + +using namespace testing; + +namespace QodeAssist { + +class MockPromptProvider : public LLMCore::IPromptProvider +{ +public: + MOCK_METHOD(LLMCore::PromptTemplate *, getTemplateByName, (const QString &), (const override)); + MOCK_METHOD(QStringList, templatesNames, (), (const override)); + MOCK_METHOD(QStringList, getTemplatesForProvider, (LLMCore::ProviderID id), (const override)); +}; + +class MockProviderRegistry : public LLMCore::IProviderRegistry +{ +public: + MOCK_METHOD(LLMCore::Provider *, getProviderByName, (const QString &), (override)); + MOCK_METHOD(QStringList, providersNames, (), (const override)); +}; + +class MockProvider : public LLMCore::Provider +{ +public: + QString name() const override { return "mock_provider"; } + QString url() const override { return "https://mock_url"; } + QString completionEndpoint() const override { return "/v1/completions"; } + QString chatEndpoint() const override { return "/v1/chat/completions"; } + bool supportsModelListing() const override { return false; } + + void prepareRequest( + QJsonObject &request, + LLMCore::PromptTemplate *promptTemplate, + LLMCore::ContextData context, + LLMCore::RequestType requestType) override + { + promptTemplate->prepareRequest(request, context); + } + + bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override + { + return true; + } + + QList getInstalledModels(const QString &url) override { return {}; } + + QStringList validateRequest( + const QJsonObject &request, LLMCore::TemplateType templateType) override + { + return {}; + } + + QString apiKey() const override { return "mock_api_key"; } + void prepareNetworkRequest(QNetworkRequest &request) const override {} + LLMCore::ProviderID providerID() const override { return LLMCore::ProviderID::OpenAI; } +}; + +class LLMClientInterfaceTest : public Test +{ +protected: + void SetUp() override + { + Core::DocumentModel::init(); + + m_provider = std::make_unique(); + m_fimTemplate = std::make_unique(); + m_chatTemplate = std::make_unique(); + m_requestHandler = std::make_unique(m_client.get()); + + ON_CALL(m_providerRegistry, getProviderByName(_)).WillByDefault(Return(m_provider.get())); + ON_CALL(m_promptProvider, getTemplateByName(_)).WillByDefault(Return(m_fimTemplate.get())); + + EXPECT_CALL(m_providerRegistry, getProviderByName(_)).Times(testing::AnyNumber()); + EXPECT_CALL(m_promptProvider, getTemplateByName(_)).Times(testing::AnyNumber()); + + m_generalSettings.ccProvider.setValue("mock_provider"); + m_generalSettings.ccModel.setValue("mock_model"); + m_generalSettings.ccTemplate.setValue("mock_template"); + m_generalSettings.ccUrl.setValue("http://localhost:8000"); + + m_completeSettings.systemPromptForNonFimModels.setValue("system prompt non fim"); + m_completeSettings.systemPrompt.setValue("system prompt"); + m_completeSettings.userMessageTemplateForCC.setValue( + "user message template prefix:\n${prefix}\nsuffix:\n${suffix}\n"); + + m_client = std::make_unique( + m_generalSettings, + m_completeSettings, + m_providerRegistry, + &m_promptProvider, + *m_requestHandler, + m_documentReader, + m_performanceLogger); + } + + void TearDown() override { Core::DocumentModel::destroy(); } + + QJsonObject createInitializeRequest() + { + QJsonObject request; + request["jsonrpc"] = "2.0"; + request["id"] = "init-1"; + request["method"] = "initialize"; + return request; + } + + QString buildTestFilePath() { return QString(CMAKE_CURRENT_SOURCE_DIR) + "/test_file.py"; } + + QJsonObject createCompletionRequest() + { + QJsonObject position; + position["line"] = 2; + position["character"] = 5; + + QJsonObject doc; + // change next line to link to test_file.py in current directory of the cmake project + doc["uri"] = "file://" + buildTestFilePath(); + doc["position"] = position; + + QJsonObject params; + params["doc"] = doc; + + QJsonObject request; + request["jsonrpc"] = "2.0"; + request["id"] = "completion-1"; + request["method"] = "getCompletionsCycling"; + request["params"] = params; + + return request; + } + + QJsonObject createCancelRequest(const QString &idToCancel) + { + QJsonObject params; + params["id"] = idToCancel; + + QJsonObject request; + request["jsonrpc"] = "2.0"; + request["id"] = "cancel-1"; + request["method"] = "$/cancelRequest"; + request["params"] = params; + + return request; + } + + Settings::GeneralSettings m_generalSettings; + Settings::CodeCompletionSettings m_completeSettings; + MockProviderRegistry m_providerRegistry; + MockPromptProvider m_promptProvider; + MockDocumentReader m_documentReader; + EmptyRequestPerformanceLogger m_performanceLogger; + std::unique_ptr m_client; + std::unique_ptr m_requestHandler; + std::unique_ptr m_provider; + std::unique_ptr m_fimTemplate; + std::unique_ptr m_chatTemplate; +}; + +TEST_F(LLMClientInterfaceTest, initialize) +{ + QSignalSpy spy(m_client.get(), &LanguageClient::BaseClientInterface::messageReceived); + + QJsonObject request = createInitializeRequest(); + m_client->sendData(QJsonDocument(request).toJson()); + + ASSERT_EQ(spy.count(), 1); + auto message = spy.takeFirst().at(0).value(); + QJsonObject response = message.toJsonObject(); + + EXPECT_EQ(response["id"].toString(), "init-1"); + EXPECT_TRUE(response.contains("result")); + EXPECT_TRUE(response["result"].toObject().contains("capabilities")); + EXPECT_TRUE(response["result"].toObject().contains("serverInfo")); +} + +TEST_F(LLMClientInterfaceTest, completionFim) +{ + // Set up the mock request handler to return a specific completion + m_requestHandler->setFakeCompletion("test completion"); + + m_documentReader.setDocumentInfo( + R"( +def main(): + print("Hello, World!") + +if __name__ == "__main__": + main() +)", + "/path/to/file.py", + "text/python"); + + QSignalSpy spy(m_client.get(), &LanguageClient::BaseClientInterface::messageReceived); + + QJsonObject request = createCompletionRequest(); + m_client->sendData(QJsonDocument(request).toJson()); + + ASSERT_EQ(m_requestHandler->receivedRequests().size(), 1); + + QJsonObject requestJson = m_requestHandler->receivedRequests().at(0).providerRequest; + ASSERT_EQ(requestJson["system"].toString(), R"(system prompt + Language: (MIME: text/python) filepath: /path/to/file.py(py) + +Recent Project Changes Context: + )"); + + ASSERT_EQ(requestJson["prompt"].toString(), R"(rint("Hello, World!") + +if __name__ == "__main__": + main() +
+def main():
+    p)");
+
+    ASSERT_EQ(spy.count(), 1);
+    auto message = spy.takeFirst().at(0).value();
+    QJsonObject response = message.toJsonObject();
+
+    EXPECT_EQ(response["id"].toString(), "completion-1");
+    EXPECT_TRUE(response.contains("result"));
+
+    QJsonObject result = response["result"].toObject();
+    EXPECT_TRUE(result.contains("completions"));
+    EXPECT_FALSE(result["isIncomplete"].toBool());
+
+    QJsonArray completions = result["completions"].toArray();
+    ASSERT_EQ(completions.size(), 1);
+    EXPECT_EQ(completions[0].toObject()["text"].toString(), "test completion");
+}
+
+TEST_F(LLMClientInterfaceTest, completionChat)
+{
+    ON_CALL(m_promptProvider, getTemplateByName(_)).WillByDefault(Return(m_chatTemplate.get()));
+
+    m_documentReader.setDocumentInfo(
+        R"(
+def main():
+    print("Hello, World!")
+
+if __name__ == "__main__":
+    main()
+)",
+        "/path/to/file.py",
+        "text/python");
+
+    m_completeSettings.smartProcessInstuctText.setValue(true);
+
+    m_requestHandler->setFakeCompletion(
+        "Here's the code: ```cpp\nint main() {\n    return 0;\n}\n```");
+
+    QSignalSpy spy(m_client.get(), &LanguageClient::BaseClientInterface::messageReceived);
+
+    QJsonObject request = createCompletionRequest();
+    m_client->sendData(QJsonDocument(request).toJson());
+
+    ASSERT_EQ(m_requestHandler->receivedRequests().size(), 1);
+
+    QJsonObject requestJson = m_requestHandler->receivedRequests().at(0).providerRequest;
+    auto messagesJson = requestJson["messages"].toArray();
+    ASSERT_EQ(messagesJson.size(), 1);
+    ASSERT_EQ(messagesJson.at(0).toObject()["content"].toString(), R"(user message template prefix:
+
+def main():
+    p
+suffix:
+rint("Hello, World!")
+
+if __name__ == "__main__":
+    main()
+
+)");
+
+    ASSERT_EQ(spy.count(), 1);
+    auto message = spy.takeFirst().at(0).value();
+    QJsonObject response = message.toJsonObject();
+
+    QJsonArray completions = response["result"].toObject()["completions"].toArray();
+    ASSERT_EQ(completions.size(), 1);
+
+    QString processedText = completions[0].toObject()["text"].toString();
+    EXPECT_TRUE(processedText.contains("# Here's the code:"));
+    EXPECT_TRUE(processedText.contains("int main()"));
+}
+
+TEST_F(LLMClientInterfaceTest, cancelRequest)
+{
+    QSignalSpy cancelSpy(m_requestHandler.get(), &LLMCore::RequestHandlerBase::requestCancelled);
+
+    QJsonObject cancelRequest = createCancelRequest("completion-1");
+    m_client->sendData(QJsonDocument(cancelRequest).toJson());
+
+    ASSERT_EQ(cancelSpy.count(), 1);
+    EXPECT_EQ(cancelSpy.takeFirst().at(0).toString(), "completion-1");
+}
+
+TEST_F(LLMClientInterfaceTest, ServerDeviceTemplate)
+{
+    EXPECT_EQ(m_client->serverDeviceTemplate().toFSPathString(), "Qode Assist");
+}
+
+} // namespace QodeAssist
diff --git a/test/MockDocumentReader.hpp b/test/MockDocumentReader.hpp
new file mode 100644
index 0000000..e3dc595
--- /dev/null
+++ b/test/MockDocumentReader.hpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2025 Povilas Kanapickas 
+ *
+ * This file is part of QodeAssist.
+ *
+ * QodeAssist is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * QodeAssist is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with QodeAssist. If not, see .
+ */
+
+#pragma once
+
+#include "context/IDocumentReader.hpp"
+#include 
+#include 
+
+namespace QodeAssist {
+
+class MockDocumentReader : public Context::IDocumentReader
+{
+public:
+    MockDocumentReader() = default;
+
+    Context::DocumentInfo readDocument(const QString &path) const override
+    {
+        return m_documentInfo;
+    }
+
+    void setDocumentInfo(const QString &text, const QString &filePath, const QString &mimeType)
+    {
+        m_document = std::make_unique(text);
+        m_documentInfo.document = m_document.get();
+        m_documentInfo.filePath = filePath;
+        m_documentInfo.mimeType = mimeType;
+    }
+
+    ~MockDocumentReader() = default;
+
+private:
+    Context::DocumentInfo m_documentInfo;
+    std::unique_ptr m_document;
+};
+
+} // namespace QodeAssist
diff --git a/test/MockRequestHandler.hpp b/test/MockRequestHandler.hpp
new file mode 100644
index 0000000..94522cb
--- /dev/null
+++ b/test/MockRequestHandler.hpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 Povilas Kanapickas 
+ *
+ * This file is part of QodeAssist.
+ *
+ * QodeAssist is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * QodeAssist is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with QodeAssist. If not, see .
+ */
+
+#pragma once
+
+#include 
+
+namespace QodeAssist::LLMCore {
+
+class MockRequestHandler : public RequestHandlerBase
+{
+public:
+    explicit MockRequestHandler(QObject *parent = nullptr)
+        : RequestHandlerBase(parent)
+        , m_fakeCompletion("")
+    {}
+
+    void setFakeCompletion(const QString &completion) { m_fakeCompletion = completion; }
+
+    void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) override
+    {
+        m_receivedRequests.append(config);
+
+        emit completionReceived(m_fakeCompletion, request, true);
+
+        QString requestId = request["id"].toString();
+        emit requestFinished(requestId, true, QString());
+    }
+
+    bool cancelRequest(const QString &id) override
+    {
+        emit requestCancelled(id);
+        return true;
+    }
+
+    const QVector &receivedRequests() const { return m_receivedRequests; }
+
+private:
+    QString m_fakeCompletion;
+    QVector m_receivedRequests;
+};
+
+} // namespace QodeAssist::LLMCore

From 8808a6a8add7b5404d2a144b22058285827052b4 Mon Sep 17 00:00:00 2001
From: Povilas Kanapickas 
Date: Mon, 10 Mar 2025 03:30:39 +0200
Subject: [PATCH 2/2] feat: Implement jinja support for user and system
 templates

The existing user and system templates will be migrated to new format
automatically.
---
 CMakeLists.txt                      |  4 ++
 LLMClientInterface.cpp              | 70 ++++++++++++++++------
 LLMClientInterface.hpp              | 10 ++++
 settings/CMakeLists.txt             |  1 +
 settings/CodeCompletionSettings.cpp | 91 +++++++++++++++++++++--------
 settings/CodeCompletionSettings.hpp | 18 ++++--
 settings/SettingsConstants.hpp      |  7 +++
 settings/SettingsUtils.hpp          |  9 +++
 test/CMakeLists.txt                 |  1 +
 test/LLMClientInterfaceTests.cpp    | 23 +++++---
 10 files changed, 180 insertions(+), 54 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 661f9c7..23f33ed 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -32,6 +32,9 @@ add_definitions(
   -DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
 )
 
+set(INJA_BUILD_TESTS OFF)
+
+add_subdirectory(3rdparty/inja)
 add_subdirectory(llmcore)
 add_subdirectory(settings)
 add_subdirectory(logger)
@@ -56,6 +59,7 @@ add_qtc_plugin(QodeAssist
     QtCreator::ExtensionSystem
     QtCreator::Utils
     QodeAssistChatViewplugin
+    inja
   SOURCES
     .github/workflows/build_cmake.yml
     .github/workflows/README.md
diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp
index 690d57f..2b2dc23 100644
--- a/LLMClientInterface.cpp
+++ b/LLMClientInterface.cpp
@@ -34,6 +34,8 @@
 #include "settings/GeneralSettings.hpp"
 #include 
 
+#include 
+
 namespace QodeAssist {
 
 LLMClientInterface::LLMClientInterface(
@@ -221,26 +223,12 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
     if (!stopWords.isEmpty())
         config.providerRequest["stop"] = stopWords;
 
-    QString systemPrompt;
-    if (m_completeSettings.useSystemPrompt())
-        systemPrompt.append(
-            m_completeSettings.useUserMessageTemplateForCC()
-                    && promptTemplate->type() == LLMCore::TemplateType::Chat
-                ? m_completeSettings.systemPromptForNonFimModels()
-                : m_completeSettings.systemPrompt());
-    if (updatedContext.fileContext.has_value())
-        systemPrompt.append(updatedContext.fileContext.value());
-
-    updatedContext.systemPrompt = systemPrompt;
+    updatedContext.systemPrompt
+        = buildSystemPrompt(promptTemplate, documentInfo, updatedContext.fileContext);
 
     if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
-        QString userMessage;
-        if (m_completeSettings.useUserMessageTemplateForCC()) {
-            userMessage = m_completeSettings.processMessageToFIM(
-                updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
-        } else {
-            userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
-        }
+        QString userMessage = buildUserMessage(
+            documentInfo, updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
 
         // TODO refactor add message
         QVector messages;
@@ -263,6 +251,52 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
     m_requestHandler.sendLLMRequest(config, request);
 }
 
+QString LLMClientInterface::buildSystemPrompt(
+    const LLMCore::PromptTemplate *promptTemplate,
+    const Context::DocumentInfo &documentInfo,
+    const std::optional &fileContext) const
+{
+    QString prompt;
+    if (m_completeSettings.useSystemPrompt()) {
+        auto templateString = m_completeSettings.useUserMessageTemplateForCC()
+                                      && promptTemplate->type() == LLMCore::TemplateType::Chat
+                                  ? m_completeSettings.systemPromptForNonFimModelsJinja()
+                                  : m_completeSettings.systemPromptJinja();
+
+        inja::json json;
+        json["mime_type"] = documentInfo.mimeType.toStdString();
+        json["language"] = Context::ProgrammingLanguageUtils::toString(
+                               Context::ContextManager::getDocumentLanguage(documentInfo))
+                               .toStdString();
+        prompt.append(QString::fromStdString(inja::render(templateString.toStdString(), json)));
+    }
+
+    if (fileContext.has_value()) {
+        prompt.append(fileContext.value());
+    }
+
+    return prompt;
+}
+
+QString LLMClientInterface::buildUserMessage(
+    const Context::DocumentInfo &documentInfo, const QString &prefix, const QString &suffix) const
+{
+    if (!m_completeSettings.useUserMessageTemplateForCC()) {
+        return prefix + suffix;
+    }
+
+    auto templateString = m_completeSettings.userMessageTemplateForCCjinja();
+
+    inja::json json;
+    json["mime_type"] = documentInfo.mimeType.toStdString();
+    json["language"] = Context::ProgrammingLanguageUtils::toString(
+                           Context::ContextManager::getDocumentLanguage(documentInfo))
+                           .toStdString();
+    json["prefix"] = prefix.toStdString();
+    json["suffix"] = suffix.toStdString();
+    return QString::fromStdString(inja::render(templateString.toStdString(), json));
+}
+
 LLMCore::ContextData LLMClientInterface::prepareContext(
     const QJsonObject &request, const Context::DocumentInfo &documentInfo)
 {
diff --git a/LLMClientInterface.hpp b/LLMClientInterface.hpp
index 122c4ed..6fe340c 100644
--- a/LLMClientInterface.hpp
+++ b/LLMClientInterface.hpp
@@ -73,6 +73,16 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
     void handleExit(const QJsonObject &request);
     void handleCancelRequest(const QJsonObject &request);
 
+    QString buildSystemPrompt(
+        const LLMCore::PromptTemplate *promptTemplate,
+        const Context::DocumentInfo &documentInfo,
+        const std::optional &fileContext) const;
+
+    QString buildUserMessage(
+        const Context::DocumentInfo &documentInfo,
+        const QString &prefix,
+        const QString &suffix) const;
+
     LLMCore::ContextData prepareContext(
         const QJsonObject &request, const Context::DocumentInfo &documentInfo);
 
diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt
index be61c3b..04307f8 100644
--- a/settings/CMakeLists.txt
+++ b/settings/CMakeLists.txt
@@ -22,5 +22,6 @@ target_link_libraries(QodeAssistSettings
     QtCreator::Core
     QtCreator::Utils
     QodeAssistLogger
+    inja
 )
 target_include_directories(QodeAssistSettings PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/settings/CodeCompletionSettings.cpp b/settings/CodeCompletionSettings.cpp
index c82500a..ebf405c 100644
--- a/settings/CodeCompletionSettings.cpp
+++ b/settings/CodeCompletionSettings.cpp
@@ -42,6 +42,9 @@ CodeCompletionSettings::CodeCompletionSettings()
 
     setDisplayName(Tr::tr("Code Completion"));
 
+    configVersion.setSettingsKey(Constants::CC_CONFIG_VERSION);
+    configVersion.setDefaultValue(Constants::CC_CONFIG_VERSION_LATEST);
+
     // Auto Completion Settings
     autoCompletion.setSettingsKey(Constants::CC_AUTO_COMPLETION);
     autoCompletion.setLabelText(Tr::tr("Enable Auto Complete"));
@@ -150,10 +153,16 @@ CodeCompletionSettings::CodeCompletionSettings()
     useSystemPrompt.setLabelText(Tr::tr("Use System Prompt"));
 
     systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT);
-    systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
-    systemPrompt.setDefaultValue(
-        "You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
+
+    systemPromptJinja.setSettingsKey(Constants::CC_SYSTEM_PROMPT_JINJA);
+    systemPromptJinja.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
+    systemPromptJinja.setDefaultValue(
+        "You are an expert {{ language }} code completion assistant. Your task is to provide "
         "precise and contextually appropriate code completions.\n\n");
+    systemPromptJinja.setToolTip(
+        Tr::tr("The setting uses Jinja format. The following keys are available:\n"
+               " - language (string): Programming language for the current file\n"
+               " - mime_type (string): MIME type of the current file\n"));
 
     useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE);
     useUserMessageTemplateForCC.setDefaultValue(true);
@@ -161,10 +170,12 @@ CodeCompletionSettings::CodeCompletionSettings()
         Tr::tr("Use special system prompt and user message for non FIM models"));
 
     systemPromptForNonFimModels.setSettingsKey(Constants::CC_SYSTEM_PROMPT_FOR_NON_FIM);
-    systemPromptForNonFimModels.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
-    systemPromptForNonFimModels.setLabelText(Tr::tr("System prompt for non FIM models:"));
-    systemPromptForNonFimModels.setDefaultValue(
-        "You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
+
+    systemPromptForNonFimModelsJinja.setSettingsKey(Constants::CC_SYSTEM_PROMPT_FOR_NON_FIM_JINJA);
+    systemPromptForNonFimModelsJinja.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
+    systemPromptForNonFimModelsJinja.setLabelText(Tr::tr("System prompt for non FIM models:"));
+    systemPromptForNonFimModelsJinja.setDefaultValue(
+        "You are an expert {{ language }} code completion assistant. Your task is to provide "
         "precise and contextually appropriate code completions.\n\n"
         "Core Requirements:\n"
         "1. Continue code exactly from the cursor position, ensuring it properly connects with any "
@@ -178,19 +189,32 @@ CodeCompletionSettings::CodeCompletionSettings()
         "- Ensure seamless integration with code both before and after the cursor\n\n"
         "Context Format:\n"
         "\n"
-        "{{code before cursor}}{{code after cursor}}\n"
+        "[[code before cursor]][[code after cursor]]\n"
         "\n\n"
         "Response Format:\n"
         "- No explanations or comments\n"
         "- Only include new characters needed to create valid code\n"
         "- Should be codeblock with language\n");
+    systemPromptForNonFimModelsJinja.setToolTip(
+        Tr::tr("The setting uses Jinja format. The following keys are available:\n"
+               " - language (string): Programming language for the current file\n"
+               " - mime_type (string): MIME type of the current file"));
 
     userMessageTemplateForCC.setSettingsKey(Constants::CC_USER_TEMPLATE);
-    userMessageTemplateForCC.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
-    userMessageTemplateForCC.setLabelText(Tr::tr("User message for non FIM models:"));
-    userMessageTemplateForCC.setDefaultValue(
+
+    userMessageTemplateForCCjinja.setSettingsKey(Constants::CC_USER_TEMPLATE_JINJA);
+    userMessageTemplateForCCjinja.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
+    userMessageTemplateForCCjinja.setLabelText(Tr::tr("User message for non FIM models:"));
+    userMessageTemplateForCCjinja.setToolTip(
+        Tr::tr("The setting uses Jinja format. The following keys are available:\n"
+               " - language (string): Programming language for the current file\n"
+               " - mime_type (string): MIME type of the current file\n"
+               " - prefix (string): The code of the current file before the cursor\n"
+               " - suffix (string): The code of the current file after the cursor"));
+
+    userMessageTemplateForCCjinja.setDefaultValue(
         "Here is the code context with insertion points:\n"
-        "\n${prefix}${suffix}\n\n\n");
+        "\n{{ prefix }}{{ suffix }}\n\n\n");
 
     useProjectChangesCache.setSettingsKey(Constants::CC_USE_PROJECT_CHANGES_CACHE);
     useProjectChangesCache.setDefaultValue(true);
@@ -222,8 +246,14 @@ CodeCompletionSettings::CodeCompletionSettings()
 
     resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
 
+    bool hasSavedConfigVersion = hasSavedSetting(&configVersion);
+
     readSettings();
 
+    if (!hasSavedConfigVersion || configVersion() < Constants::CC_CONFIG_VERSION_1_JINJA_TEMPLATES) {
+        upgradeOldTemplatesToJinja();
+    }
+
     readFileParts.setValue(!readFullFile.value());
 
     setupConnections();
@@ -252,13 +282,13 @@ CodeCompletionSettings::CodeCompletionSettings()
         auto contextItem = Column{
             Row{contextGrid, Stretch{1}},
             Row{useSystemPrompt, Stretch{1}},
-            Group{title(Tr::tr("Prompts for FIM models")), Column{systemPrompt}},
+            Group{title(Tr::tr("Prompts for FIM models")), Column{systemPromptJinja}},
             Group{
                 title(Tr::tr("Prompts for Non FIM models")),
                 Column{
                     Row{useUserMessageTemplateForCC, Stretch{1}},
-                    systemPromptForNonFimModels,
-                    userMessageTemplateForCC,
+                    systemPromptForNonFimModelsJinja,
+                    userMessageTemplateForCCjinja,
                 }},
             Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}};
 
@@ -347,23 +377,38 @@ void CodeCompletionSettings::resetSettingsToDefaults()
         resetAspect(readStringsBeforeCursor);
         resetAspect(readStringsAfterCursor);
         resetAspect(useSystemPrompt);
-        resetAspect(systemPrompt);
+        resetAspect(systemPromptJinja);
         resetAspect(useProjectChangesCache);
         resetAspect(maxChangesCacheSize);
         resetAspect(ollamaLivetime);
         resetAspect(contextWindow);
         resetAspect(useUserMessageTemplateForCC);
-        resetAspect(userMessageTemplateForCC);
-        resetAspect(systemPromptForNonFimModels);
+        resetAspect(userMessageTemplateForCCjinja);
+        resetAspect(systemPromptForNonFimModelsJinja);
     }
 }
 
-QString CodeCompletionSettings::processMessageToFIM(const QString &prefix, const QString &suffix) const
+void CodeCompletionSettings::upgradeOldTemplatesToJinja()
 {
-    QString result = userMessageTemplateForCC();
-    result.replace("${prefix}", prefix);
-    result.replace("${suffix}", suffix);
-    return result;
+    {
+        QString newTemplate = userMessageTemplateForCC();
+        newTemplate.replace("${prefix}", "{{ prefix }}");
+        newTemplate.replace("${suffix}", "{{ suffix }}");
+        userMessageTemplateForCCjinja.setValue(newTemplate);
+    }
+    {
+        QString newTemplate = systemPromptForNonFimModels();
+        newTemplate.replace(
+            "{{code before cursor}}{{code after cursor}}",
+            "[[code before cursor]][[code after cursor]]");
+        newTemplate.replace("C++, Qt, and QML", "{{ language }}");
+        systemPromptForNonFimModelsJinja.setValue(newTemplate);
+    }
+    {
+        QString newTemplate = systemPrompt();
+        newTemplate.replace("C++, Qt, and QML", "{{ language }}");
+        systemPromptJinja.setValue(newTemplate);
+    }
 }
 
 class CodeCompletionSettingsPage : public Core::IOptionsPage
diff --git a/settings/CodeCompletionSettings.hpp b/settings/CodeCompletionSettings.hpp
index c871e8d..3c4a64f 100644
--- a/settings/CodeCompletionSettings.hpp
+++ b/settings/CodeCompletionSettings.hpp
@@ -32,6 +32,8 @@ class CodeCompletionSettings : public Utils::AspectContainer
 
     ButtonAspect resetToDefaults{this};
 
+    Utils::IntegerAspect configVersion{this}; // used to track config version for upgrades
+
     // Auto Completion Settings
     Utils::BoolAspect autoCompletion{this};
     Utils::BoolAspect multiLineCompletion{this};
@@ -65,10 +67,10 @@ class CodeCompletionSettings : public Utils::AspectContainer
     Utils::IntegerAspect readStringsBeforeCursor{this};
     Utils::IntegerAspect readStringsAfterCursor{this};
     Utils::BoolAspect useSystemPrompt{this};
-    Utils::StringAspect systemPrompt{this};
+    Utils::StringAspect systemPromptJinja{this};
     Utils::BoolAspect useUserMessageTemplateForCC{this};
-    Utils::StringAspect systemPromptForNonFimModels{this};
-    Utils::StringAspect userMessageTemplateForCC{this};
+    Utils::StringAspect systemPromptForNonFimModelsJinja{this};
+    Utils::StringAspect userMessageTemplateForCCjinja{this};
     Utils::BoolAspect useProjectChangesCache{this};
     Utils::IntegerAspect maxChangesCacheSize{this};
 
@@ -79,11 +81,17 @@ class CodeCompletionSettings : public Utils::AspectContainer
     // API Configuration Settings
     Utils::StringAspect apiKey{this};
 
-    QString processMessageToFIM(const QString &prefix, const QString &suffix) const;
-
 private:
+    // The aspects below only exist to fetch configuration from settings produced
+    // by old versions of the plugin. This configuration is used to fill in new versions
+    // of these settings so that users do not need to upgrade manually.
+    Utils::StringAspect systemPrompt{this};
+    Utils::StringAspect systemPromptForNonFimModels{this};
+    Utils::StringAspect userMessageTemplateForCC{this};
+
     void setupConnections();
     void resetSettingsToDefaults();
+    void upgradeOldTemplatesToJinja();
 };
 
 CodeCompletionSettings &codeCompletionSettings();
diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp
index cdf33a0..e04d05f 100644
--- a/settings/SettingsConstants.hpp
+++ b/settings/SettingsConstants.hpp
@@ -54,6 +54,7 @@ const char CC_PRESET1_URL_HISTORY[] = "QodeAssist.ccPreset1UrlHistory";
 
 // settings
 const char ENABLE_QODE_ASSIST[] = "QodeAssist.enableQodeAssist";
+const char CC_CONFIG_VERSION[] = "QodeAssist.ccConfigVersion";
 const char CC_AUTO_COMPLETION[] = "QodeAssist.ccAutoCompletion";
 const char ENABLE_LOGGING[] = "QodeAssist.enableLogging";
 const char ENABLE_CHECK_UPDATE[] = "QodeAssist.enableCheckUpdate";
@@ -107,9 +108,12 @@ const char CC_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.ccReadStringsBeforeCurs
 const char CC_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.ccReadStringsAfterCursor";
 const char CC_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt";
 const char CC_SYSTEM_PROMPT[] = "QodeAssist.ccSystemPrompt";
+const char CC_SYSTEM_PROMPT_JINJA[] = "QodeAssist.ccSystemPromptJinja";
 const char CC_SYSTEM_PROMPT_FOR_NON_FIM[] = "QodeAssist.ccSystemPromptForNonFim";
+const char CC_SYSTEM_PROMPT_FOR_NON_FIM_JINJA[] = "QodeAssist.ccSystemPromptForNonFimJinja";
 const char CC_USE_USER_TEMPLATE[] = "QodeAssist.ccUseUserTemplate";
 const char CC_USER_TEMPLATE[] = "QodeAssist.ccUserTemplate";
+const char CC_USER_TEMPLATE_JINJA[] = "QodeAssist.ccUserTemplateJinja";
 const char CC_USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.ccUseProjectChangesCache";
 const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize";
 const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt";
@@ -143,4 +147,7 @@ const char CA_OLLAMA_LIVETIME[] = "QodeAssist.chatOllamaLivetime";
 const char CA_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.caOllamaContextWindow";
 const char CA_API_KEY[] = "QodeAssist.chatApiKey";
 
+const int CC_CONFIG_VERSION_1_JINJA_TEMPLATES = 1;
+const int CC_CONFIG_VERSION_LATEST = CC_CONFIG_VERSION_1_JINJA_TEMPLATES;
+
 } // namespace QodeAssist::Constants
diff --git a/settings/SettingsUtils.hpp b/settings/SettingsUtils.hpp
index 51b9f59..212b705 100644
--- a/settings/SettingsUtils.hpp
+++ b/settings/SettingsUtils.hpp
@@ -67,6 +67,15 @@ void resetAspect(AspectType &aspect)
     aspect.setVolatileValue(aspect.defaultValue());
 }
 
+inline bool hasSavedSetting(Utils::BaseAspect *aspect)
+{
+    auto settings = aspect->qtcSettings();
+    if (!settings)
+        return false;
+
+    return settings->contains(aspect->settingsKey());
+}
+
 inline void initStringAspect(
     Utils::StringAspect &aspect,
     const Utils::Key &settingsKey,
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 1dbeaf7..f7b4530 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -15,6 +15,7 @@ target_link_libraries(QodeAssistTest PRIVATE
     GTest::Main
     QtCreator::LanguageClient
     Context
+    inja
 )
 
 target_compile_definitions(QodeAssistTest PRIVATE CMAKE_CURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
diff --git a/test/LLMClientInterfaceTests.cpp b/test/LLMClientInterfaceTests.cpp
index 94efbe9..36eda18 100644
--- a/test/LLMClientInterfaceTests.cpp
+++ b/test/LLMClientInterfaceTests.cpp
@@ -114,10 +114,11 @@ class LLMClientInterfaceTest : public Test
         m_generalSettings.ccTemplate.setValue("mock_template");
         m_generalSettings.ccUrl.setValue("http://localhost:8000");
 
-        m_completeSettings.systemPromptForNonFimModels.setValue("system prompt non fim");
-        m_completeSettings.systemPrompt.setValue("system prompt");
-        m_completeSettings.userMessageTemplateForCC.setValue(
-            "user message template prefix:\n${prefix}\nsuffix:\n${suffix}\n");
+        m_completeSettings.systemPromptForNonFimModelsJinja.setValue(
+            "system prompt non fim for {{ language }} end");
+        m_completeSettings.systemPromptJinja.setValue("system prompt {{ language }} end");
+        m_completeSettings.userMessageTemplateForCCjinja.setValue(
+            "user message template prefix:\n{{ prefix }}\nsuffix:\n{{ suffix }}\n");
 
         m_client = std::make_unique(
             m_generalSettings,
@@ -223,7 +224,7 @@ if __name__ == "__main__":
     main()
 )",
         "/path/to/file.py",
-        "text/python");
+        "text/x-python");
 
     QSignalSpy spy(m_client.get(), &LanguageClient::BaseClientInterface::messageReceived);
 
@@ -233,8 +234,8 @@ if __name__ == "__main__":
     ASSERT_EQ(m_requestHandler->receivedRequests().size(), 1);
 
     QJsonObject requestJson = m_requestHandler->receivedRequests().at(0).providerRequest;
-    ASSERT_EQ(requestJson["system"].toString(), R"(system prompt
- Language:  (MIME: text/python) filepath: /path/to/file.py(py)
+    ASSERT_EQ(requestJson["system"].toString(), R"(system prompt python end
+ Language: python (MIME: text/x-python) filepath: /path/to/file.py(py)
 
 Recent Project Changes Context:
  )");
@@ -276,7 +277,7 @@ if __name__ == "__main__":
     main()
 )",
         "/path/to/file.py",
-        "text/python");
+        "text/x-python");
 
     m_completeSettings.smartProcessInstuctText.setValue(true);
 
@@ -291,6 +292,12 @@ if __name__ == "__main__":
     ASSERT_EQ(m_requestHandler->receivedRequests().size(), 1);
 
     QJsonObject requestJson = m_requestHandler->receivedRequests().at(0).providerRequest;
+    ASSERT_EQ(requestJson["system"].toString(), R"(system prompt non fim for python end
+ Language: python (MIME: text/x-python) filepath: /path/to/file.py(py)
+
+Recent Project Changes Context:
+ )");
+
     auto messagesJson = requestJson["messages"].toArray();
     ASSERT_EQ(messagesJson.size(), 1);
     ASSERT_EQ(messagesJson.at(0).toObject()["content"].toString(), R"(user message template prefix: