De hier geïllustreerde ontwikkeling valt binnen de categorie « RAG » (Retrieval-Augmented Generation) van artificiële intelligentie. De term « RAG » verwijst doorgaans naar retrieval-augmented generation, een AI-kader waarin een large language model (LLM) eerst relevante informatie ophaalt uit een private kennisbron alvorens een antwoord te genereren. Dit proces laat de AI toe contextueel aangepaste antwoorden te geven. Deze bron zal bestaan uit enkele Word-bestanden waarvan de inhoud overeenkomt met een interne kennisbank.
Voor deze ontwikkeling is de installatie van een Qdrant-server vereist, die zal dienen als VectorStore voor het opslaan van de kennisbankinhoud.
Dit vereist de installatie van de plugins MercatorAI.OpenAI en MercatorAI.Qdrant, en werkt enkel met een Core-versie van Mercator. (De code is echter ook compileerbaar in een klassieke versie).
Daarna worden twee afzonderlijke C#-codes opgezet: de eerste om de VectorStore te vullen of bij te werken en de tweede om te chatten met OpenAI op basis van deze inhoud.
Deze twee codes maken gebruik van deze klasse, die als model zal dienen voor de VectorStore:
public sealed class DocChunk
{
[MercatorAI.FactoryQdrant.VectorStoreKey]
public Guid Id { get; set; } = Guid.NewGuid();
[MercatorAI.FactoryQdrant.VectorStoreData(IsIndexed = true)]
public string SourceId { get; set; }
[MercatorAI.FactoryQdrant.VectorStoreData]
public int Chunk { get; set; }
[MercatorAI.FactoryQdrant.VectorStoreData(IsFullTextIndexed = true)]
public string Content { get; set; } = "";
// Modus "auto": we slaan een string-eigenschap op die gemarkeerd is als vector.
// De waarde is de tekst waarvan automatisch een embedding wordt gegenereerd.
[MercatorAI.FactoryQdrant.VectorStoreVector(Dimensions = 1536)] // 1536 voor text-embedding-3-small
public string Embedding => Content;
}
De eigenschap SourceId is gemarkeerd als geïndexeerd. Dit maakt het mogelijk een voorafgaand filter te plaatsen om de zoekopdracht te beperken tot een specifiek brondocument. Dit is optioneel.
De inhoud van de eigenschap Content wordt gevectoriseerd en opgeslagen in Embedding.
1. De VectorStore vullen of bijwerken
In het weergegeven voorbeeld laden we een reeks Word-bestanden (extensie DOCX) die zich in de Mercator-database bevinden. Deze inhoud wordt opgesplitst in stukken (chunks) van ideale grootte voor het beoogde gebruik. Voor het berekenen van deze chunks maakt Mercator gebruik van Microsoft.SemanticKernel. In de code zien we dat de chunks door OpenAI in embeddings (vectoren) worden omgezet, dus door een externe dienst, ook al worden ze opgeslagen op een private Qdrant-server.
Opmerking: de opsplitsing in chunks is eenvoudig. Een complexer proces kan voordelig zijn, rekening houdend met de structuur van het document.
Omdat de collectie een willekeurige identifier bevat via Guid.NewGuid(), wordt ze bij een update telkens verwijderd. Het is ook mogelijk een identifier te gebruiken die een bekende informatie bevat om enkel de toegevoegde of gewijzigde records bij te werken. (Bijvoorbeeld met Api.DeterministicGuidFromString).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MercatorUi;
using MercatorApi;
using MercatorExtensions;
// <CompileWithRoslyn />
namespace MyAssembly
{
public static class ChatGPT
{
private const string tutosCollectionName = "Tutos";
public async static Task VectorizeTutos()
{
if (!Dialogs.AnswerYesNo("De inhoud van de tutorials echt vectoriseren?"))
return;
MercatorAI.FactoryOpenAI factoryOpenAI = new MercatorAI.FactoryOpenAI();
MercatorAI.Interfaces.IOpenAiEmbeddingGeneratorClient openAiEmbeddingGeneratorClient = factoryOpenAI.CreateOpenAiEmbeddingGeneratorClient<string>("text-embedding-3-small", out string error);
if (!string.IsNullOrEmpty(error))
{
Dialogs.Stop("Kan de embeddings-generator van OpenAI niet initialiseren: " + error);
return;
}
MercatorAI.FactoryQdrant factoryQdrant = new MercatorAI.FactoryQdrant();
using (MercatorAI.Interfaces.IQdrantClient qdrantClient = factoryQdrant.CreateQdrantClient("http://192.168...:6334", out error))
{
if (!string.IsNullOrEmpty(error))
{
Dialogs.Stop("Kan de Qdrant-client niet initialiseren: " + error);
return;
}
MercatorUi.Wait.WaitStatic.WaitWindow(Api.Iif_langue(Globals.Langue, IifLangueEnum.DataUpdate));
await qdrantClient.CollectionDeleteAsync<Guid, DocChunk>(tutosCollectionName);
using (var embeddingGenerator = openAiEmbeddingGeneratorClient.EmbeddingGenerator)
using (var collection = await qdrantClient.CreateCollectionAsync<Guid, DocChunk>(tutosCollectionName, embeddingGenerator))
{
foreach (string docxFile in Api.GetFiles("<Other\\Tutos", "*.docx"))
{
string sourceId = Api.JustFName(docxFile);
string fullText = Api.DocxToText(docxFile);
List<string> chunks = qdrantClient.TextChunkerSplitPlainTextLines(fullText, maxTokensPerLine: 400);
IEnumerable<DocChunk> records = chunks.Select((text, i) => new DocChunk
{
Chunk = i,
SourceId = sourceId,
Content = text
});
foreach (DocChunk[] arrayOfDocChunks in records.Chunk(500)) // we verzenden per blok van 500
{
await qdrantClient.CollectionUpsertAsync<Guid, DocChunk>(collection, arrayOfDocChunks);
}
}
}
MercatorUi.Wait.WaitStatic.WaitClear();
Dialogs.Stop(Api.Iif_langue(Globals.Langue, IifLangueEnum.UpdateAchieved));
}
}
}
}
Deze code kan bijvoorbeeld worden aangeroepen vanuit het lint (Voer C#-code async uit):
public static class Script
{
public static async Task ExecAsync(DevComponents.DotNetBar.ButtonItem clickedButtonItem)
{
// enter your customized code here
await MyAssembly.ChatGPT.VectorizeTutos();
}
}
of worden uitgevoerd vanuit Mercator in consolemodus. In dat geval moeten de Dialogs.Stop(...) worden vervangen door MercatorUi.Globals.MercatorTasksToMain.Log(..., isError: true) en moeten alle regels die beginnen met MercatorUi.Wait.WaitStatic worden verwijderd.
2. Chatten met OpenAI op basis van deze inhoud
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MercatorUi;
using MercatorApi;
using MercatorExtensions;
// <CompileWithRoslyn />
namespace MyAssembly
{
public static class ChatGPT
{
private const string tutosCollectionName = "Tutos";
public static async Task SearchInTutos()
{
MercatorAI.FactoryOpenAI factoryOpenAI = new MercatorAI.FactoryOpenAI();
MercatorAI.Interfaces.IOpenAiEmbeddingGeneratorClient openAiEmbeddingGeneratorClient = factoryOpenAI.CreateOpenAiEmbeddingGeneratorClient<string>("text-embedding-3-small", out string error);
if (!string.IsNullOrEmpty(error))
{
Dialogs.Stop("Kan de embeddings-generator van OpenAI niet initialiseren: " + error);
return;
}
MercatorAI.FactoryQdrant factoryQdrant = new MercatorAI.FactoryQdrant();
MercatorAI.Interfaces.IQdrantClient qdrantClient = factoryQdrant.CreateQdrantClient("http://192.168...:6334", out error);
if (!string.IsNullOrEmpty(error))
{
Dialogs.Stop("Kan de Qdrant-client niet initialiseren: " + error);
return;
}
MercatorUi.Wait.WaitStatic.WaitWindow(Api.Iif_langue(Globals.Langue, IifLangueEnum.AIisThinking));
if (!await qdrantClient.CollectionExistsAsync(tutosCollectionName))
{
qdrantClient.Dispose();
MercatorUi.Wait.WaitStatic.WaitClear();
Dialogs.Stop($"De collectie \"{tutosCollectionName}\" bestaat niet op de Qdrant-server!");
return;
}
MercatorUi.Forms.Other.ChatForm chatForm = MercatorUi.Forms.Other.ChatForm.SimpleOpenAiChat("gpt-4o", tutosCollectionName, systemMessage: null, question: "", false, chatOptions: null, standardStreamingAction: MercatorUi.Forms.Other.ChatForm.StandardStreamingActionEnum.RawStreamShow);
MercatorUi.Wait.WaitStatic.WaitClear();
if (chatForm == null)
{
qdrantClient.Dispose();
return;
}
chatForm.Disposed += (s, e) =>
{
qdrantClient.Dispose();
};
MercatorUi.Forms.Other.ChatForm.BeforeAskEventHandler beforeAskEventHandler = null;
beforeAskEventHandler = (s, e) =>
{
if (string.IsNullOrWhiteSpace(chatForm.TextBoxQuestion.Text))
{
_Divers.FocusError(chatForm.TextBoxQuestion);
return;
}
(double? score, DocChunk docChunk)[] hits = null;
MercatorUi.Wait.WaitStatic.WaitWindowBaseThread(Api.Iif_langue(Globals.Langue, IifLangueEnum.AIisThinking));
MercatorUi._BaseClasses.ExclusiveBackgroundWorkerAsync exclusiveBackgroundWorkerAsync = new MercatorUi._BaseClasses.ExclusiveBackgroundWorkerAsync(async () =>
{
float[] questionAsVectors = await openAiEmbeddingGeneratorClient.GenerateEmbeddingAsync(chatForm.TextBoxQuestion.Text);
hits = await qdrantClient.ClientSearchAsync<Guid, DocChunk>(tutosCollectionName, questionAsVectors, top: 5);
}, formWhereToCenterLoadingCircle: chatForm);
MercatorUi.Wait.WaitStatic.WaitClearBaseThread();
if (exclusiveBackgroundWorkerAsync.ExceptionDuringDoWork != null)
{
Dialogs.Stop(exclusiveBackgroundWorkerAsync.ExceptionDuringDoWork.Message, chatForm);
return;
}
if ((hits == null) || !hits.Any())
{
Dialogs.Stop("Geen overeenkomst gevonden in de documentatie!", chatForm);
return;
}
var sb = new StringBuilder();
foreach ((double? score, DocChunk docChunk) hit in hits)
{
sb.AppendLine(hit.docChunk.Content);
}
chatForm.SimpleChatSystemMessage = "Je bent een assistent. Antwoord uitsluitend op basis van de CONTEXT die is gehaald uit tutorials waarin bepaalde boekhoudprocedures in ons bedrijf worden uitgelegd. Als de info er niet is, zeg dit dan duidelijk. \nCONTEXT:\n" + sb;
chatForm.BeforeAsk -= beforeAskEventHandler; // SimpleChatSystemMessage enkel bij de eerste interactie in de chat
};
chatForm.BeforeAsk += beforeAskEventHandler;
}
}
}
Deze code toont een ChatForm van Mercator. Het belangrijkste gebeurt in beforeAskEventHandler, d.w.z. vóór de uitvoering van de code wanneer men voor de eerste keer op de knop "Ondervragen" klikt. Op dat punt zien we dat de code de VectorStore bevraagt om enkele resultaten op te halen die relevant worden geacht voor de gestelde vraag. Deze informatie wordt samengevoegd en eenvoudig meegegeven in het systeembericht van de chat. In de code zien we dat de gestelde vraag door OpenAI wordt omgezet in embeddings (vectoren), dus door een externe dienst, ook al steunt de configuratie op een private Qdrant-server.
Deze code kan bijvoorbeeld worden aangeroepen vanuit het lint (Voer C#-code async uit):
public static class Script
{
public static async Task ExecAsync(DevComponents.DotNetBar.ButtonItem clickedButtonItem)
{
// enter your customized code here
await MyAssembly.ChatGPT.SearchInTutos();
}
}
De informatie wordt verzonden naar een derde partij (OpenAI). Er mogen dus geen vertrouwelijke gegevens worden doorgestuurd. Deze documentatie geeft duidelijk aan dat de dataset in zijn geheel naar OpenAI wordt gestuurd om embeddings (vectoren) te genereren.
De antwoorden die door deze code worden gegenereerd, zijn automatisch gegenereerd met behulp van een model voor kunstmatige intelligentie (AI). Hoewel het ontworpen is om nuttige en relevante antwoorden te geven, kunnen deze antwoorden onvolledig, onjuist of gedeeltelijk fout zijn.
De gebruiker blijft als enige verantwoordelijk voor de evaluatie en het gebruik van deze antwoorden. Mercator of de leverancier van de AI-dienst kan in geen geval aansprakelijk worden gesteld voor de gevolgen van het gebruik van deze antwoorden.
Het gebruik van deze dienst moet compatibel zijn met de GDPR-voorschriften die binnen het bedrijf van toepassing zijn.