Build a RAG command line chatbot (C#)
This tutorial demonstrates how to build a command line chatbot. The chatbot uses data from your Astra collection for retrieval-augmented generation (RAG) with OpenAI.
This example uses the collection of book summaries from the quickstart, but you can use any collection of documents that have the $vectorize field populated with the text you want used as context when answering questions.
Prerequisites
-
An active Serverless (vector) database.
-
An application token with the Database Administrator role.
-
A collection in your database with documents that have the
$vectorizefield populated.If you don’t already have this, follow the quickstart.
-
An OpenAI API key.
-
.NET version 8 or later, .NET Framework 4.6.2 or later, or .NET Standard 2.1 or later.
Store your credentials
For this tutorial, store your database’s Data API endpoint, application token, and OpenAI API key in environment variables:
- Linux or macOS
-
export API_ENDPOINT=API_ENDPOINT export APPLICATION_TOKEN=APPLICATION_TOKEN export OPENAI_API_KEY=OPENAI_API_KEY - Microsoft Windows
-
set API_ENDPOINT=API_ENDPOINTset APPLICATION_TOKEN=APPLICATION_TOKENset OPENAI_API_KEY=OPENAI_API_KEY
Install packages
Install the astra-db-csharp and OpenAI packages.
For example:
dotnet add package DataStax.AstraDB.DataApi OpenAI
Add the code
using System.Text;
using DataStax.AstraDB.DataApi;
using DataStax.AstraDB.DataApi.Admin;
using DataStax.AstraDB.DataApi.Collections;
using DataStax.AstraDB.DataApi.Core;
using DataStax.AstraDB.DataApi.Core.Query;
using OpenAI.Chat;
public class Program
{
public static async Task Main()
{
const string keyspace = "default_keyspace"; (1)
const string collectionName = "quickstart_collection"; (2)
string? endpoint = Environment.GetEnvironmentVariable("API_ENDPOINT"); (3)
string? applicationToken = Environment.GetEnvironmentVariable(
"APPLICATION_TOKEN"
);
string? openaiApiKey = Environment.GetEnvironmentVariable(
"OPENAI_API_KEY"
);
if (
string.IsNullOrEmpty(endpoint)
|| string.IsNullOrEmpty(applicationToken)
|| string.IsNullOrEmpty(openaiApiKey)
)
{
throw new InvalidOperationException(
"Environment variables API_ENDPOINT, APPLICATION_TOKEN, OPENAI_API_KEY must be defined."
);
}
// Instantiate the DataAPIClient and get a reference to your collection
var client = new DataAPIClient();
var database = client.GetDatabase(
endpoint,
new GetDatabaseOptions()
{
Token = applicationToken,
Keyspace = keyspace,
}
);
var collection = database.GetCollection(collectionName);
// Instantiate the OpenAI client
var openai = new ChatClient(
model: "gpt-4o-mini",
apiKey: openaiApiKey
);
// This list of messages will be sent to OpenAI with every query.
// It starts with a single system prompt and grows as the chat progresses.
var messages = new List<ChatMessage>
{
new SystemChatMessage(
"You are an AI assistant that can answer questions based on the the context you are given. "
+ "Don't mention the context, just use it to inform your answers."
),
};
// Start the chat by writing a message to the CLI
Console.Write(
"Greetings! I am an AI assistant that is ready to help you with your questions. "
+ "You can ask me anything you like.\n"
+ "If you want to exit, type \".exit\".\n\n> "
);
string? userInput = Console.ReadLine();
// Run this loop continuously until the user inputs the exit command
while (
userInput != null
&& !userInput.Equals(".exit", StringComparison.OrdinalIgnoreCase)
)
{
// If the user didn't input text, re-prompt them
if (string.IsNullOrWhiteSpace(userInput))
{
Console.Write("> ");
userInput = Console.ReadLine();
continue;
}
try
{
// Perform a vector search in your collection,
// using the user input as the search string to vectorize.
// Limit the search to 10 documents.
// Use a projection to return just the $vectorize field of each document.
var response = collection.Find(
new CollectionFindOptions<Document>()
{
Sort = Builders<Document>.CollectionSort.Vectorize(userInput),
Projection = Builders<Document>.Projection.Include(
"$vectorize"
),
}
);
// Join the $vectorize fields of the returned documents into a single string
var contextBuilder = new StringBuilder();
await foreach (var document in response)
{
if (
document.TryGetValue("$vectorize", out var value)
&& value is string text
)
{
contextBuilder.AppendLine(text);
}
}
string context = contextBuilder.ToString().TrimEnd();
// Combine the user question with the context from vector search
var ragMessage = new UserChatMessage(
$"{context}\n---\nGiven the above context, answer the following question:\n{userInput}"
);
// Send the list of previous messages, plus the context augmented question
// to OpenAI and stream the response
var requestMessages = new List<ChatMessage>(messages)
{
ragMessage,
};
var stream = openai.CompleteChatStreamingAsync(requestMessages);
// Write OpenAI's response to the CLI as it comes in,
// and also record it in a string
var message = new StringBuilder();
await foreach (var update in stream)
{
if (update.ContentUpdate.Count > 0)
{
string chunk = update.ContentUpdate[0].Text ?? "";
Console.Write(chunk);
message.Append(chunk);
}
}
// Record the user question, without the added context, in the list of messages
messages.Add(new UserChatMessage(userInput));
// Record the OpenAI response in the list of messages
messages.Add(new AssistantChatMessage(message.ToString()));
// Prompt the user for their next question
Console.Write("\n\n> ");
userInput = Console.ReadLine();
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
Console.Write("\nSomething went wrong, try asking again\n\n> ");
userInput = Console.ReadLine();
}
}
}
}
| 1 | Change the keyspace name if your collection is in a different keyspace. |
| 2 | Change the collection name if you are not using the collection created in the quickstart. |
| 3 | Store your database’s endpoint, application token, and OpenAI key in environment variables named API_ENDPOINT, APPLICATION_TOKEN, and OPENAI_API_KEY, as instructed in Store your credentials. |
Test the code
-
From your terminal, run the code from the previous section.
The terminal should show the welcome message and a
>prompt. -
Enter a question. For example,
Can you recommend a book set on another planet?The terminal should print the answer from OpenAI, and give the
>prompt again. -
To exit, type
.exit.
Next steps
-
If you used the the quickstart collection, try making a collection of other data and using that instead.
-
If the user asks a question unrelated to the collection contents, the vector search still returns 10 documents. However, the similarity scores for these documents will be low, and the documents won’t be relevant to the question.
In this tutorial, similarity scores are not requested, and low-similarity results are still included in the context that is sent to OpenAI.
You can use the
includeSimilarityoption to return a similarity score for each document. Then, you can omit results with a low similarity score from the context, or prompt the user to ask a more relevant question. -
In this tutorial, the message list grows infinitely as the chat progresses. You can truncate the message list so that older messages are discarded.