This article describes a companion app used in the Generative AI curriculum for JavaScript that was recently released. See a creative use of a system message to bring history to life
DISCLAIMER: this article showcase an interesting way to interact with historical characters using Generative AI. The responses from the characters are based on their training data and does not represent their actual thoughts or opinions.
This article describes a companion app used in the Generative AI curriculum for JavaScript that was recently released. The app allows users to interact with historical characters by setting its system message to a historical character plus some added context and instructions.
How it works
For Generative AI response use GitHub Models, a great way to test models for free, all you need is a GitHub account. Below code works when run inside of a GitHub Codespace but can be made to work with a personal access token, a so called PAT.
Here's a link to a GitHub Models playground where you can test our different models and grab code snippets to use in your own projects: GitHub Models Playground
How our app calls GitHub Models
The app calls a GitHub Model API to generate a response. Here's how that piece looks:
const messages = [
{ "role": "system", "content": systemMessage, },
{ "role": "user", "content": prompt } ];
const openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com",
apiKey: process.env.GITHUB_TOKEN,
});
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: messages,
});
console.log(`SERVER: ${completion.choices[0]?.message?.content}`);
The preceding code snippet shows:
- The messages array that contains the "system" message and the user prompt.
- An instance of the OpenAI class that is initialized with the baseURL and apiKey.
- The openai.chat.completions.create method that generates a response based on the model and messages provided.
- The response is then logged to the console.
The system message
The system message instructs the LLM to act as a historical character. To create many character to switch between we use a characters.json file to store system messages and other information about the character. Here's an example of the characters.json file:
{
"title": "Dinocrates",
"name": "dinocrates",
"description": "You are Dinocrates of Alexandria, a famous architect and engineer. Limit your responses to only the time you live in, you don't know anything else. You only want to talk about your architecture and engineering projects, and possibly new ideas you have.",
"image": "dinocrates.png",
"avatar": "dinocrates-avatar.jpeg"
}
Let's highlight the system message:
"You are Dinocrates of Alexandria, a famous architect and engineer. Limit your responses to only the time you live in, you don't know anything else. You only want to talk about your architecture and engineering projects, and possibly new ideas you have."
With such a system message, the LLM will generate responses like it is Dinocrates of Alexandria. Note also how additional instructions are added like:
- Limit your responses to only the time you live in.
- You don't know anything else.
- You only want to talk about your architecture and engineering projects, and possibly new ideas you have.
With this prompt, you avoid that the LLM generates responses that are out of character for Dinocrates.
Accessibility
It's important to make apps accessible to everyone. The app uses the SpeechSynthesis API to read the responses out loud. This also creates a nice effect of hearing the historical character speak.
NOTE: But again, remember our disclaimer at the beginning of this article as it's important to be responsible when using Generative AI.
Here's how the code looks like for the SpeechSynthesis API:
// what to say
let text = "a response coming back from the LLM";
let currentCharacter = characters[0];
let utterance = new SpeechSynthesisUtterance(text);
utterance.voice = speechSynthesis.getVoices()[currentCharacter.voice || 0];
utterance.rate = 1;
utterance.pitch = 1;
utterance.volume = 1;
// utter the text
speechSynthesis.speak(utterance);
How it works:
- The text variable contains the response coming back from the LLM.
- The currentCharacter variable contains the current character information.
- An instance of the SpeechSynthesisUtterance class is created with the text.
- The voice property is set to the voice of the current character.
- The rate, pitch, and volume properties are set to 1. Adjust this to your liking.
- The speechSynthesis.speak method is called with the utterance instance.
The web part of the app
The app is a simple web app that uses HTML, CSS, and JavaScript. Let's see how we use a call to fetch to make a web request to our API that in turn makes a call to the GitHub Model API:
fetch('/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
})
.then(response => response.json())
.then(data => {
responseWindow.value += `\n<%= title %>: ${data.answer}`;
console.log("speak enabled", speakEl.checked);
if (speakEl.checked) {
say(data.answer); }
})
.catch(error => { console.error('Error:', error.error); });
In the preceding code, we:
- Call fetch with the /send endpoint.
- Set the method to POST.
- Set the Content-Type header to application/json.
- Set the body to the message object. Message contains the user input.
- Call the then method with a callback that logs the response to the console.
- Call the then method with a callback that appends the response to the responseWindow textarea. Now the user can see the response.
The API is an Express app with a /send endpoint that calls the GitHub Model API. Here's how the /send endpoint looks:
app.post('/send', async (req, res) => {
const { message, character } = req.body;
systemMessage = character.description;
const prompt = message;
const messages = [ {
"role": "system",
"content": systemMessage,
},
{
"role": "user",
"content": prompt
} ];
const openai = new OpenAI({
baseURL: "https://models.inference.ai.azure.com",
apiKey: process.env.GITHUB_TOKEN,
});
try {
console.log(`SERVER sending prompt ${prompt}`)
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: messages,
});
console.log(`SERVER: ${completion.choices[0]?.message?.content}`);
res.json({
prompt: prompt,
answer: completion.choices[0]?.message?.content
});
} catch (error) {
console.log(`Error: ${error}`);
res.status(500).json({ error: error });
}
});
Here, we:
- Destructure the message and character from the request body.
- Set the systemMessage to the character.description.
- Set the prompt to the message.
- Create the messages array with the system message and the user message.
- Create an instance of the OpenAI class with the baseURL and apiKey.
- Call the openai.chat.completions.create method with the model and messages.
- Send the response back to the client.
The final app
The finished app is a simple web app with the following structure:
- index.html: The main HTML file.
- public folder containing important files:
- styles.css: The CSS file.
- characters.json We store the system messages and other information about the characters.
- script.js: The JavaScript file.
- imgages/ containing avatar images, small ones for the chat and larger ones displayed to the user.
Here's what it looks like:
Conclusion
As you can see, you don't need to do much to create an app that makes things come to life.
If you want to have a conversation with your data, you can use Generative AI to make it happen.
Just remember to be responsible by letting your end users know that the responses are generated and not real answers from the historical characters or any other character you might use.