Table of contents
Not long ago, I discovered a small LLM created by Nvidia, and the beginning of the model's description was: "Nemotron-Mini-4B-Instruct is a model for generating responses for roleplaying." Wow!, I had to check right away if I could have fun with that. It would be fine if I could chat with a Dwarf or an Elf 🥰
At this end of the year, I've decided to get more serious about Python, so I'll be using Ollama's Python SDK, which is very accessible.
So, you will need the Ollama dependency (https://github.com/ollama/ollama-python)
pip install ollama
The source code of the project is here https://github.com/parakeet-nest/npc-agent, I did a DevContainer project, so you don't need to install anything if you use it as is (except for Ollama and VSCode).
Pull the nemotron-mini
LLM:
ollama pull nemotron-mini
We are ready to start.
The character sheet of a Dwarf
As I'm lazy, I'm going to ask Nemotron to generate a character name for me and then automatically create a character sheet that it will use for the chat.
This is the source code (01-character-sheet/main.py
):
import json
from ollama import Client
class Character:
def __init__(self, name, race):
self.name = name
self.race = race
name_instructions = """
You are an expert NPC generator for games like D&D 5th edition.
You have freedom to be creative to get the best possible output.
"""
name_user_tpl = """generate a random name for a role-playing game character for a {species}.
The output should be in JSON format, with the keys 'name' and 'race'.
Ensure the name is fantasy-themed.
"""
description_instructions = """# IDENTITY and PURPOSE
You are an expert NPC generator for games like D&D 5th edition.
You have freedom to be creative to get the best possible output.
"""
description_steps = """# STEPS
Generate a complete character sheet for a non-player character (NPC) in a fantasy role-playing game.
The character should be unique and well-developed, with the following details:
1. **race**: The given race.
2. **Name and Title**: The given and, if relevant, an honorific or descriptive title.
3. **Age**: Indicate the character's age.
4. **Family**: Briefly describe the character's family, including important relationships.
5. **Occupation**: What is the character's main occupation or role? Describe their skills and their role in society.
6. **Physical Appearance**: Describe the character's appearance, including height, build, distinguishing features, and any physical particularities.
7. **Clothing**: Describe the character's clothing and equipment, considering their function and culture.
8. **Food Preferences**: What are the character's food preferences? Include a specific detail that makes them unique.
9. **Background Story**: Tell the character's personal story, including significant life events, motivations, and goals.
10. **Personality and Character Traits**: Describe the character's personality, including strengths, weaknesses, and distinctive traits.
11. **Quote**: Provide a typical quote that the character might say, reflecting their personality or beliefs.
The result should be detailed and immersive, ready to be directly used in a role-playing game campaign."
# OUTPUT INSTRUCTIONS
- Output in clear, human-readable Markdown.
Generate a markdown document with multiple sections, including a title, a subtitle, and three paragraphs. Each section should be separated by a blank line (carriage return). Ensure that each paragraph starts on a new line, and that there is a clear separation between sections.
**Expected Output:**
```markdown
# Title of the Document
## Subtitle of the Document
This is the first paragraph. It provides an introduction to the topic.
This is the second paragraph. It delves deeper into the details of the topic.
This is the third paragraph. It concludes the discussion and offers final thoughts.
```
"""
def generate_character(species: str, ollama_url: str, model: str) -> Character:
ollama_client = Client(host=ollama_url)
instruction_content = name_instructions
user_content = name_user_tpl.format(species=species)
print(f"🤖 user message for name generation> {user_content}")
completion = ollama_client.chat(
model=model,
messages=[
{'role': 'system', 'content': instruction_content},
{'role': 'user', 'content': user_content},
],
options={"temperature":0.3},
stream=False,
format="json",
)
json_character = completion['message']['content']
print(json_character + "\n")
# Parse the JSON string to a dictionary
character_data = json.loads(json_character)
# Create a Character object from the dictionary
character = Character(name=character_data['name'], race=character_data['race'])
return character
def generate_description(character: Character, ollama_url: str, model: str) -> str:
instruction_content = description_instructions
steps_content = description_steps
user_content = "Create a {race} with this name: {name}".format(race=character.race, name=character.name)
print(f"🤖 user message for description generation> {user_content}")
ollama_client = Client(host=ollama_url)
stream = ollama_client.chat(
model=model,
messages=[
{'role': 'system', 'content': instruction_content},
{'role': 'system', 'content': steps_content},
{'role': 'user', 'content': user_content},
],
options={
"temperature":0.5,
"repeat_last_n":3,
"repeat_penalty":2.0,
"top_k":10,
"top_p":0.5,
},
stream=True,
)
answer = ""
for chunk in stream:
content = chunk['message']['content']
answer+=content
print(content, end='', flush=True)
# save content of answer to a markdown file: description.md
# Open the file in write mode
with open('./description-'+character.race+'.md', "w") as file:
# Write the content to the file
file.write(answer)
print("\n")
model="nemotron-mini"
ollama_url="http://localhost:11434"
character = generate_character("dwarf", ollama_url, model)
generate_description(character, ollama_url, model)
Quick explanation:
First, define a
Character
class with basic attributes like name and race.There are two main functions:
generate_character
: This function creates a character name and race.generate_description
: This function creates a detailed character description.
The
generate_character
function:Sends a request to the AI model to generate a character name and race.
The AI's response is expected to be in JSON format.
It then creates a
Character
object with this information.
The
generate_description
function:Takes the created character and asks the AI to generate a detailed description.
The description includes various aspects like age, family, occupation, appearance, etc.
The generated description is streamed (output bit by bit) and saved to a Markdown file.
At the end (bottom) of the script:
Set up the model name ("
nemotron-mini
") and the Ollama API URL.Generate a Dwarf character.
Then generate a detailed description for this character.
To generate the character sheet, type this command:
python main.py
You should get this document: description-dwarf.md
with a content like this one:
# Gimli the Stout
## Gimli the Stout
Gimli is a Dwarf, hailing from the Misty Mountains of the East.
He is a stout and burly warrior, known for his strength and resilience.
Gimli is 30 years old, and he is the eldest son of Thorin Oakenshield,
the leader of Thorin's Company.
### Gimli the Stout's Family
Gimli's family is a prominent one in the Dwarf community.
His father, Thorin Oakenshield, is the leader of Thorin's Company,
a group of Dwarves who have been on the run for many years,
trying to reclaim their homeland from the evil forces of the Misty Mountains.
Gimli is the eldest son of Thorin, and his siblings include Kili, Bofur and Bombu.
### Gimli the Stout's Occupation
As the eldest son of Thorin Oakenshield, Gimli is expected to take over
the leadership of Thorin's Company when his father is no longer able to do so.
However, Gimli is also a skilled warrior and a formidable fighter,
and he is known to be a fierce and loyal defender of his people.
### Gimli the Stout's Physical Appearance
Gimli is a stout and burly Dwarf, standing at 6 feet tall and weighing around 30 stone.
He has a thick beard and a muscular build, and his eyes are a piercing shade of blue.
Gimli has a few scars on his face and arms, but he is not known to be a violent person.
### Gimli the Stout's Clothing
Gimli wears the traditional Dwarven armor, including a heavy breastplate,
greaves for his legs, and a helm for his head.
He also wears a long, flowing cloak made of thick wool,
which he uses to keep himself warm during the long and cold journey.
### Gimli the Stout's Food Preferences
Gimli has a simple taste in food, preferring hearty and filling meals,
such as roasted meats, stews made with Dwarven ale, and hearty bread.
However, he is known to enjoy a good cup of Dwarven ale,
and he is always willing to share a drink with his friends.
### Gimli the Stout's Background Story
Gimli was born in the Misty Mountains, during a time of great hardship for his people.
His father, Thorin Oakenshield, was forced to flee his homeland
and seek refuge in the Lonely Mountain, where he and his company
were forced to live in hiding for many years.
Gimli grew up in the shadow of his father's leadership,
and he was taught the importance of loyalty, courage and resilience.
### Gimli the Stout's Personality and Character Traits
Gimli is a loyal and brave Dwarf, who is always willing to stand up
for his people and defend them against any threat.
He is also a skilled warrior, and he is known to be a fierce and ruthless fighter.
However, Gimli is also a kind and gentle soul,
who is always willing to lend a hand to those in need.
### Gimli the Stout's Quote
"The Misty Mountains may be a harsh and unforgiving place,
but we Dwarves are hardy and resilient.
We will face whatever comes our way, and we will come out on top."
Time to chat with a Dwarf
Let’s create a new Python source code.
This is the source code (02-chat-with-character/main.py
):
import json
from ollama import Client
class Character:
def __init__(self, name, race):
self.name = name
self.race = race
chat_instruction = """You are a {race}, your name is {name},
you are an expert at interpreting and answering questions based on provided sources.
Using only the provided context, answer the user's question
to the best of your ability using only the resources provided.
Be verbose!
"""
def chat_with_character(character: Character, description: str,ollama_url: str, model: str):
ollama_client = Client(host=ollama_url)
instructions = chat_instruction.format(race=character.race, name=character.name)
msg = "🤖 [{name}] (type 'bye' to exit):> ".format(name=character.name)
while True:
user_input = input(msg)
if user_input.lower() == "bye":
print("👋 Goodbye!")
break
else:
stream = ollama_client.chat(
model=model,
messages=[
{'role': 'system', 'content': instructions},
{'role': 'system', 'content': description},
{'role': 'user', 'content': user_input},
],
options={"temperature":0.5},
stream=True,
keep_alive=1,
)
for chunk in stream:
print(chunk['message']['content'], end='', flush=True)
print("\n")
model="nemotron-mini"
ollama_url="http://localhost:11434"
character = Character(name="Gimli the Stout", race="dwarf")
with open('../01-character-sheet/description-'+character.race+'.md', 'r') as file:
description = file.read()
chat_with_character(character, description, ollama_url, model)
Quick explanation:
Import necessary libraries and defines a
Character
class with name and race attributes.The
chat_instruction
is a template for the AI to assume the role of the character.The main function
chat_with_character
:Sets up a connection to the Ollama API.
Starts a loop for user interaction.
For each user input, it sends a request to the AI model, including:
The character instructions
The character's description (loaded from a file)
The user's input
It then streams and prints the AI's response.
The chat continues until the user types '
bye
'.At the end of the script:
Set up the model and API URL.
Create a character named "
Gimli the Stout
".Loads the character's description from a file.
Starts the chat interaction.
Let’s do it!
Start the chat:
python main.py
🤖 [Gimli the Stout] (type 'bye' to exit):> hello, what is your name?
My name is Gimli the Stout.
🤖 [Gimli the Stout] (type 'bye' to exit):> what is your main catch phrase?
"The Misty Mountains may be a harsh and unforgiving place,
but we Dwarves are hardy and resilient.
We will face whatever comes our way, and we will come out on top."
🤖 [Gimli the Stout] (type 'bye' to exit):> where do you come from?
I am a Dwarf hailing from the Misty Mountains of the East.
🤖 [Gimli the Stout] (type 'bye' to exit):> how old are you?
I am 30 years old.
🤖 [Gimli the Stout] (type 'bye' to exit):> who is your father?
Thorin Oakenshield
Honestly, I find the results mind-blowing. Now I need to work on the request settings for Ollama and improve the prompts. I should add something to maintain conversational memory (Nemotron supports embeddings). I'm also wondering if I could manage some sort of combat with the character using dices (Nemotron supports tools). But that will be for later.
Have fun with the bots 🤖