All parts of HW8 are due Wed Mar 11th at 11:55 pm.
This project is a modification of Chris Piech's neat AI project. Chris teaches CS109.
The idea with this project is we have an adventure story stored in a dict. The story has different scenes, and the user can go from one scene to another. The trick is, AI is used to come up with new scenes as needed, so the story actually just goes on forever. So in part, this project gives realistic practice with dictionaries and function, and of course it's neat to get to see AI integrated to provide such an audacious feature.
Downloads infinite-story.zip to get started.
To get started, go to not open ai to get an API key string.
Once you have your key, paste your notopenai key to replace your-key-here in this line
CLIENT = NotOpenAI(api_key="your-key-here")
Aside: Why is the API called NotOpenAI? It's just a joke. OpenAI is the company that created GPT-3.5. To keep things free for you we are routing all of your requests through our CS106A paid account. We call it NotOpenAI because we are not OpenAI 🙂. The API is identical to the OpenAI API, and if you want to switch over to your own paid OpenAI key, you can!
You also need to install "Pillow" and also the "requests" module. The following line will take care of both:
$ python3 -m pip install Pillow requests
First take a look at the "story" structure. The structure is a bit complicate, but realistic nested dict structure. The "story" is the outer dict, containing elements to build an adventure story. Within the story is a "scenes" key, which points to a dict of all the scenes. Each scene is itself a dict, known by its key in the scenes dict. In the example below, from the file "tiny.json", the scenes are 'start' and 'scene_aaa'. Each scene contains a "choices" list, and each choice is a little dict holding a scene_key of a scene the user might go to. In the tiny.json example, from the "start" scene, it's possible to go to either 'scene_aaa' or 'scene_bbb'.
{
"plot": "plot words blah blah blah",
"scenes": {
"start": {
"text": "You are at the start.",
"scene_summary": "Words about start.",
"choices": [
{
"text": "aaa choice text",
"scene_key": "scene_aaa"
},
{
"text": "bbb choice text",
"scene_key": "scene_bbb"
}
]
},
"scene_aaa": {
"text": "aaa text",
"scene_summary": "blah",
"choices": [
{
"text": "Go back to the start",
"scene_key": "start"
},
{
"text": "ccc choice text",
"scene_key": "scene_ccc"
}
]
}
}
}
It will turn out to be interesting when a scene_key is referenced, but there is no such key in the story. We'll call this an "unknown" scene_key.
Complete code for the is_unknown_scene() function which checks if a key is unknown or not. Doctests are provided. The story dict put together for the Doctest is incredibly small, with just a couple scenes, and leaving out everything else not needed for this test.
def is_unknown_scene(story, scene_key):
"""
Return True if this scene_key is not present in the story scenes,
i.e. it is an unknown scene.
>>> story = {'plot': 'xyz', 'scenes': {'start': {}, 'go_outside': {}}}
>>> is_unknown_scene(story, 'go_outside')
False
>>> is_unknown_scene(story, 'selfie_with_celeb')
True
"""
pass
Complete code for the unknown_scenes() function. Given a story,
look at all the scenes, and within them, all the choices.
Return a list of all the unknown scene_keys from among the choices.
Doctests are provided. One test uses the 'tiny.json' example shown above, which
should return the list ['scene_bbb', 'scene_ccc']. The other test
uses the 'original_small.json' story, and you can look at that
file to see its scene details.
def unknown_scenes(story):
"""
Look at all the scenes, and within them,
all the choices. Return a list of all the scene_key
strings which are unknown.
>>> story = json.load(open(f'data/tiny.json'))
>>> unknown_scenes(story)
['scene_bbb', 'scene_ccc']
>>> story = json.load(open(f'data/original_small.json'))
>>> unknown_scenes(story)
['next_to_gully', 'descend_into_valley', 'watching_sunset', 'continue_exploring_hilltop', 'return_to_small_brick_building']
"""
pass
This function calls the AI to create a new scene when the player goes to a scene that does not exist yet.
The boilerplate code to call the AI is provided, but your code needs to construct the prompt string.
The prompt string should have the following contents, with text you need to include shown in italics. Get data you need from inside the story dict, e.g. the 'start' scene.
Return the next scene of a story for key: scene-key. An example scene should be formatted in json like this: json-form-of-start-scene-dict. The main plot line of the story is: plot text from story.
The provided code sends this prompt to the AI which should compose a new scene dict, which the function returns. This function does not have tests, so you'll have to see how it works later on.
As the user goes through the story, at each turn, the program needs to provide the next scene they see — this may be a scene that already exists in the story, or perhaps the program needs to create a new scene. This function handles this central next-scene operation. Once this function is done, the program should be able to run.
def next_scene(story, scene_key):
"""
Given a story and scene_key. If the scene_key is known,
simple return that scene from the story. If the scene_key is
unknown construct a new scene dict via the AI, insert the
new scene dict into the story, and then return the new scene.
Complex Doctests are provided.
"""
pass
If the scene already exists in the story, then simply return it. If the scene is unknown, (1) call the AI to create a new scene, (2) insert this new scene in to the story structure, (3) return the new scene. In all cases, the caller gets the new scene to use, and behind the scenes it may be newly created or not.
We provide complex Doctests for this function to help you get it done. Here is the test for the case that the scene is known:
>>> # 1. if scene is known, simply return it
>>> story = json.load(open('data/tiny.json'))
>>> scene = next_scene(story, 'scene_aaa')
>>> scene['text']
'aaa text'
Here are the tests for the case that the scene_key is unknown, and so a new scene should be created:
... story = json.load(open('data/tiny.json'))
... scene = next_scene(story, 'scene_bbb')
... # Assert two things about newly created scene
... # 1. next_scene() returns the new-scene-placeholder
... assert scene == {'text': 'new_scene_placeholder'}
... # 2. story dict is modified, scene_bbb is now in its scenes
... scenes = story['scenes']
... assert 'scene_bbb' in scenes
... assert scenes['scene_bbb'] == {'text': 'new_scene_placeholder'}
The Doctest uses the advanced "mock" technique: the test temporarily replaces the real create_new_scene() function with code that simply returns this literal placeholder dict: {'text': 'new_scene_placeholder'}. The "assert" lines above then each check a condition that should be true after your function runs. If one of them is False, the Doctest will fail on that line with an AssertionError.
1. The Doctest calls next_scene() with the unknown key 'scene_bbb', and verifies that the placeholder dict is returned, thus verifying that next_scene() is calling create_new_scene() and using the scene dict it returns.
2. The Doctest then checks that the placeholder has been added inside story['scenes'], since adding the new scene into the story is the other requirement for this function.
The code to construct mock functions like this is beyond the scope of CS106A, so you can just observe the data pattern the tests verify. The idea is that we temporarily replace the create_new_scene() with a "mock" function that just returns the placeholder. In this way, we're able to test next_scene() thoroughly, but without calling or depending on the whole stack of AI-invoking code.
The other functions are provided. They call your next_scene() and the other functions to make the story work.
Run the program like this:
$ python3 infinite_story.py data/original_small.json
The stories for running are original_small.json original_big.json, and engineer_story.json. The syntax 'data/original_small.json' is the way to refer to the file 'original_small.json' inside the folder 'data'. Use the tab-key to autocomplete parts of the filenames on the command line.
When it prompts the user to select their next scene, a '*' next to the option marks that that will be an AI generated room (using your is_unknown_scene() function of course!).
A graphics window will pop up when the program starts, showing some DallE graphics for the scenes in the story files. Move the graphics window off to the side, as you type commands in the terminal window. There are only graphics pre-made for a few of the early scenes, and as AI starts making up new scenes, the graphics window will just be a blue rectangle. When you see the blue rectangle, it does not mean you have done anything wrong, instead you have gone beyond the few pre-made scene graphics we have. The graphics files are simply stored in the 'img' folder. Take a look in there if you are curious. It would be easy for you to add your own images if you want to create a story.
Explore around, see how the AI does. You can mess around with our stories or write your own.
As a last step, we have some ethics reflections
There's going to be AI in your future, so we would like to establish some truisms you can keep in mind. The AI training reads in vast quantities of text, and much of this has an English language and culture flavor. The patterns and biases in those sources are baked into the model the AI uses to produce results.
The following prompt, provided in the starter code, asks the AI to add to a story about family that lives next to a woods, and the parents tell the children to not leave the house.
# The prompt for the --folk feature.
FOLK_PROMPT = '''Return this story with five lines, and give the children names.
The result should be formatted in json like this:
{"title": "story title",
"lines": ["line 1 of story", "line 2 of story", "more lines"]}.
The beginning of the story is:
{"title": "Child story",
"lines": ["Once there was a poor family with two children living next to a woods.",
"The parents told the children to stay inside while the parents were out."]}
'''
Here is the command to run the AI to work on this story. Run this command three times (no coding is required). Look at the three stories by the AI.
$ python3 infinite_story.py -folk
What you will likely see is that the AI produces a story structure that echoes traditional European folk tales, such as Hansel and Gretel or Little Red Riding Hood.
The prompt does not mention folk tales, but the AI homes in on the pattern in its training data. The children often disobey, and encounter some fairy-tale situation in the woods. There are many possible stories one could tell about two children left at home, but the pattern in training data is irresistible to the AI, so this is what we get.
Remember this: the AI is first trained on a body of data. That data naturally has patterns and biases, and it's hard for the AI to avoid these in its output.
Open the file "infinite_ethics.txt". Look at your AI output, and pull out the names the AI uses for the children. For Q1 in the file, enter these names. Most of the world does not use names like this! The names are a shorthand reminder that the AI output is tailored by its input training data.
Here is Q2 to answer in the file: Q2 - What are some issues can you imagine occurring if instead of asking AI to generate a story, you used AI to evaluate candidates for a job? We do not need a very long answer, just showing a little thought about the dynamics of AI.
Please turn in your infinite_story.py and infinite_ethics.txt files on on Paperless as usual.
This neat project was first built by Chris Piech and others, and then Nick Parlante adopted and modified it in 2024, adding in Doctests, later adding the folk-tale example, and providing more of the boilerplate to make a smaller assignment. In 2026 added the complex mock tests to next_scene() to help students test that more thoroughly. Here is the acknowledgement from Chris's original version:
Assignment designed by Chris Piech, inspired by Eric Roberts. Handout written with Anjali Sreenivas, Yasmine Alonso, Katie Liu. Ethics by Javokhir Arifov and Dan Webber. Test scripts by Iddah Mlauzi and Tina Zheng. Advised by Mehran Sahami and Ngoc Nguyen, and more!