Project 2: llmPlay: Where in the World?
DUE Wed, 10/29, 2 pm
This programming assignment (PA) may be completed individually or in teams of at most 2. You can partner differently for each PA.
We will build a game that let user play guess-the-city game with an LLM. When the user guessed right, the LLM sends the city’s lat/lon to the app, which then moves the map camera to center on the city.
Due to the limitations of smaller LLMs, we found that the gemma3:12b model is
the smallest that can play this game. We have provided access to this model for
the completion of this assignment. See the Testing section below.
Treat your messages sent to chatterd and Ollama as public utterances
with no reasonable expectation of privacy and know that these
are recorded for the purposes of carrying out a contextual interaction
with Ollama and are potentially shared with everyone using chatterd.
Objectives
In addition to the objectives listed in the llmChat and
Maps tutorials, this PA has the following objectives:
- Practice and apply the objectives learned in the two tutorials
- On the backend, generate a new SSE event to pass lat/lon data as a distinct event
- On the frontend, handle a new SSE lat/lon event and reposition displayed map to the new lat/lon
Features and requirements
Your app must provide the following features and satisfy the following requirements, including those in any applicable “Implementation guidelines” documents, to receive full credit.
Front-end UI
As can be seen in the video in either of the front-end specs, the app consists of a single screen with the following UI elements:
- a title bar showing the title
Where in the world?, - a map that initially shows the device’s current location, with a current location “blue dot,” on the map
- these UI elements at the bottom of the screen:
- a text box spanning the left and middle part of the input area,
-
a “Send” button on the right of the textbox showing a “paper plane” icon. This button is enabled only when the text box is not empty and no networking session is in progress.
When the button is “disabled”, they are grayed out and tapping on it has no effect.
While there is a networking session in progress, that is, while waiting for Ollama’s hints or evaluation of user’s guesses, the “Send” button’s icon changes from showing a “paper plane” to showing an animated “loading” circle.
UI Design
One can easily spend a whole weekend (or longer) getting the UI “just right.”
Remember: we won’t be grading you on how beautiful your UI
looks nor how precisely it matches the one shown on the video demo. You’re
free to design your UI differently, so long as all indicated UI elements
are fully visible on the screen, non overlapping, and functioning as specified.
Front-end UX
As demonstrated in the video above:
- Upon loading, the app frontend does two things automatically:
- displays a map showing the user’s current location, and
- initiates the game by sending a
STARTmessage to the LLM, throughchatterd, to retrieve the first hints to guess a city.
-
Hints from the LLM will be displayed above the text box. User enters their guess in the textbox and clicks “Send” to post it to the LLM.
-
If the guess is wrong, LLM replies with another set of hints and user can enter another guess. (LLM will reveal the solution if you enter “I give up,” or some other similar statement.)
- If the guess is right, the app will receive the lat/lon of the city
in a SSE
LatLonevent. The app then moves the map camera to center on the lat/lon of the city.
API
We introduce a new API endpoint, which we call llmplay. Its protocol handshake and data formats are mosty similar to the llmchat endpoint documented in the llmchat specification. Please consult that document if
you haven’t done the tutorial. In addition to the llmchat protocol,
when the user made a correct guess in the game, the llmplay API
handler returns a LatLon SSE event with the following data line:
event: latlon
data: { "lat": double, "lon": double }
We will not be using the postmaps and getmaps APIs from the maps
tutorial.
Back-end infrastructures
-
Upon receiving a message, the backend checks for existence of an
appIDon the message. Notably, the backend does not check for uniqueness ofappIDs; rather, anappIDcan be re-used to resume a conversation with Ollama. -
The backend stores each message from a frontend in its PostgreSQL database, tagged with the message’s
appID. When Ollama responds to a prompt, the backend also stores the completion tagged with the sameappID. When forwarding a prompt to Ollama, the backend retrieves and sends all messages with the sameappID, in chronological order, to Ollama, forming the prompt’s context/history. -
Upon receiving a
STARTmessage, the backend enters it into the PostgreSQL database, tagged with the frontend’sappID. This hard-coded prompt is the first entry in “context” sent to Ollama accompanying each prompt with the sameappID. -
Ollama streams replies as newline-delimited JSON (NDJSON) streams. The backend transforms these into SSE streams. Prior-and-post an NDJSON stream, the backend communicates errors to the frontend as HTTP errors. Once an NDJSON stream has started, however, any error, e.g., database error, will be communicated to the frontend as an SSE
errorevent alongside themessageevent. -
In the
STARTprompt, Ollama is instructed to send a message following a specific format if the user made a correct guess: “WINNER!!!:lat:lon:”, where thelatandlonare of the city in question. In addition to forwarding this message along, the backend also sends an SSELatLonevent with a JSON Object containing the lat/lon in the event’s data line.Sometimes the LLM “changes its mind” as to the identity of the mystery city part way through a game! This happens mostly after multiple rounds of wrong guesses from the user.
Implementation and submission guidelines
Backend
For the backend, regardless of the stack of choice, you should build off
the code base from the llmchat tutorial.
First, make a new API endpoint for llmplay, with its eponymous handler.
If you make a copy of the llmchat() URL handler to serve as the basis
for llmplay(), there are basically a small number of changes you
need to make:
-
I hard-code the following prompt to be sent to Ollama on the start of a game,
“Think of a city. Do not tell the user the name of the city, give the user 2 to 3 hints at a time and let the user guess. When the user guesses right, say the following in the provided format, along with the lat/lon of the city, and then ask the user if they want to play again: WINNER!!!:lat:lon:”.
For the app to work correctly, it is imperative that you specify the exact format for the winning notification, including the colons.
-
If the message from the frontend contains the word “START”, I insert the prompt into the
messagecolumn of the database, tagged with theappIDfrom the frontend, using “system” as theusername, this will alert Ollama that the message contains instructions on how it should interact with the user. Otherwise I insert the message into the database as I do in thellmchattutorial. -
Once an NDJSON stream from Ollama concludes, after inserting the full response into the database, I check the response for the “WINNER!!!” string. If it exists as a substring of the full response, I split the full response using “:” as delimiter and retrieve the lat/lon from the full response as the second and third elements in the split string. The function to split a string by a delimiter is invariably called
split()in all the languages used in the backend (with capitalization of the first letter in Go). To check wether a substring is in a string, you useContains()in Go,inin Python,contains()in Rust, andincludes()in TypeScript.
As usual, git commit your changes to your chatterd source files with the
commit message, "pa2 back end", and push your changes to your git repo.
Frontend
Due to the location access and permission request to do so, you would want to
build your frontend off the maps tutorial front end. However, for ChattStore,
we don’t need postChatt and getChatts, instead
you want to build off llmChat from the llmchat frontend.
More detailed guidelines for the two frontend platforms are available in the following separate specs:
Testing
Frontend
We found LLM model smaller than gemma3:12b to not be able to play this
guess the city game. To test your frontend, you will use mada.eecs.umich.edu,
specifying gemma3:12b model.
Unfortunately, Ollama can only hold one conversation at a time. If you have
a host with sufficient resources, you may want to pull model gemma3:12b (8GB)
to Ollama running on your host for your own use.
Backend
For grading purposes, please pull gemma3:1b (815 MB) to your AWS/GCP backend
instance. You may want to remove tinyllama first:
server$ ollama rm tinyllama
server$ ollama pull gemma3:1b
Test your backend with the following test case. The “system” message
is the START prompt to use in place of the one above:
{
"model": "gemma3:1b",
"messages": [
{ "role": "system", "content": "Repeat after user verbatim." },
{ "role": "user", "content": "WINNER!!!!!:39.91:-79.47" }
],
"stream": true
}
For grading purposes, please leave your chatterd backend running on your instance with the above as the START prompt.
From Postman or using curl, the above should return, “WINNER!!!!!:39.91:-79.47”.
Using the above as the START prompt, a working frontend should immediately move its camera to center on the vicinity of Fallingwater, PA upon launch.
We found gemma3:1b to at least be able to follow instructions to return the
winner notification string verbatim; tinyllama cannot. On a *-micro instance
of AWS or GCP, however, it could take Ollama over 2 and a half minutes (or longer) to reply. So be patient.
WARNING: You will not get full credit if your front end is not set
up to work with your backend!
Everytime you rebuild your Go or Rust server or make changes to either of your
JavaScript or Python files, you need to restart chatterd:
server$ sudo systemctl restart chatterd
Leave your chatterd running until you have received your tutorial grade.
TIP:
server$ sudo systemctl status chatterd
is your BEST FRIEND in debugging your server. If you get an HTTP error code 500 Internal Server Error or if you just don’t know whether your HTTP request has made it to the server, first thing you do is run sudo systemctl status chatterd on your server and study its output.
If you’re running a Python server, it also shows error messages from your Python code, including any debug printouts from your code. The command systemctl status chatterd is by far the most useful go-to tool to diagnose your back-end server problem.
| Prepared by Chenglin Li, Xin Jie ‘Joyce’ Liu, Sugih Jamin | Last updated: August 8th, 2025 |