Make Any Catalog-Driven App More Personalized to Your Users: How I used Generative AI Coding Tools to Improve a Go App With Gemini.

How many chatbots do we really need? While chatbots are a terrific example app for generative AI use cases, I’ve been thinking about how developers may roll generative AI into existing “boring” apps and make them better.

As I finished all my Christmas shopping—much of it online—I thought about all the digital storefronts and how they provide recommended items based on my buying patterns, but serve up the same static item descriptions, regardless of who I am. We see the same situation with real estate listings, online restaurant menus, travel packages, or most any catalog of items! What if generative AI could create a personalized story for each item instead? Wouldn’t that create such a different shopping experience?

Maybe this is actually a terrible idea, but during the Christmas break, I wanted to code an app from scratch using nothing but Google Cloud’s Duet AI while trying out our terrific Gemini LLM, and this seemed like a fun use case.

The final app (and codebase)

The app shows three types of catalogs and offers two different personas with different interests. Everything here is written in Go and uses local files for “databases” so that it’s completely self-contained. And all the images are AI-generated from Google’s Imagen2 model.

When the user clicks on a particular catalog entry, the go to a “details” page where the generic product summary from the overview page is sent along with a description of the user’s preferences to the Google Gemini model to get a personalized, AI-powered product summary.

That’s all there is to it, but I think it demonstrates the idea.

How it works

Let’s look at what we’ve got here. Here’s the basic flow of the AI-augmented catalog request.

How did I build the app itself (GitHub repo here)? My goal was to only use LLM-based guidance either within the IDE using Duet AI in Google Cloud, or burst out to Bard where needed. No internet searches, no docs allowed.

I started at the very beginning with a basic prompt.

What are the CLI commands to create a new Go project locally?

The answer offered the correct steps for getting the project rolling.

The next commands are where AI assistance made a huge difference for me. With this series of natural language prompts in the Duet AI chat within VS Code, I got the foundation of this app set up in about five minutes. This would have easily taken me 5 or 10x longer if I did it manually.

Give me a main.go file that responds to a GET request by reading records from a local JSON file called property.json and passes the results to an existing html/template named home.html. The record should be defined in a struct with fields for ID, Name, Description, and ImageUrl.
Create an html/template for my Go app that uses Bootstrap for styling, and loops through records. For each loop, create a box with a thin border, an image at the top, and text below that. The first piece of text is "title" and is a header. Below that is a short description of the item. Ensure that there's room for four boxes in a single row.
Give me an example data.json that works with this struct
Add a second function to the class that responds to HTML requests for details for a given record. Accept a record id in the querystring and retrieve just that record from the array before sending to a different html/template

With these few prompts, I had 75% of my app completed. Wild! I took this baseline, and extended it. The final result has folders for data, personas, images, a couple HTML files, and a single main.go file.

Let’s look at the main.go file, and I’ll highlight a handful of noteworthy bits.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"strconv"

	"github.com/google/generative-ai-go/genai"
	"google.golang.org/api/option"
)

// Define a struct to hold the data from your JSON file
type Record struct {
	ID          int
	Name        string
	Description string
	ImageURL    string
}

type UserPref struct {
	Name        string
	Preferences string
}

func main() {

	// Parse the HTML templates
	tmpl := template.Must(template.ParseFiles("home.html", "details.html"))

	//return the home page
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

		var recordType string
		var recordDataFile string
		var personId string

		//if a post-back from a change in record type or persona
		if r.Method == "POST" {
			// Handle POST request:
			err := r.ParseForm()
			if err != nil {
				http.Error(w, "Error parsing form data", http.StatusInternalServerError)
				return
			}

			// Extract values from POST data
			recordType = r.FormValue("recordtype")
			recordDataFile = "data/" + recordType + ".json"
			personId = r.FormValue("person")

		} else {
			// Handle GET request (or other methods):
			// Load default values
			recordType = "property"
			recordDataFile = "data/property.json"
			personId = "person1" // Or any other default person
		}

		// Parse the JSON file
		data, err := os.ReadFile(recordDataFile)
		if err != nil {
			fmt.Println("Error reading JSON file:", err)
			return
		}

		var records []Record
		err = json.Unmarshal(data, &records)
		if err != nil {
			fmt.Println("Error unmarshaling JSON:", err)
			return
		}

		// Execute the template and send the results to the browser
		err = tmpl.ExecuteTemplate(w, "home.html", struct {
			RecordType string
			Records    []Record
			Person     string
		}{
			RecordType: recordType,
			Records:    records,
			Person:     personId,
		})
		if err != nil {
			fmt.Println("Error executing template:", err)
		}
	})

	//returns the details page using AI assistance
	http.HandleFunc("/details", func(w http.ResponseWriter, r *http.Request) {

		id, err := strconv.Atoi(r.URL.Query().Get("id"))
		if err != nil {
			fmt.Println("Error parsing ID:", err)
			// Handle the error appropriately (e.g., redirect to error page)
			return
		}

		// Extract values from querystring data
		recordType := r.URL.Query().Get("recordtype")
		recordDataFile := "data/" + recordType + ".json"

		//declare recordtype map and extract selected entry
		typeMap := make(map[string]string)
		typeMap["property"] = "Create an improved home listing description that's seven sentences long and oriented towards a a person with these preferences:"
		typeMap["store"] = "Create an updated paragraph-long summary of this store item that's colored by these preferences:"
		typeMap["restaurant"] = "Create a two sentence summary for this menu item that factors in one or two of these preferences:"
		//get the preamble for the chosen record type
		aiPremble := typeMap[recordType]

		// Parse the JSON file
		data, err := os.ReadFile(recordDataFile)
		if err != nil {
			fmt.Println("Error reading JSON file:", err)
			return
		}

		var records []Record
		err = json.Unmarshal(data, &records)
		if err != nil {
			fmt.Println("Error unmarshaling JSON:", err)
			return
		}

		// Find the record with the matching ID
		var record Record
		for _, rec := range records {
			if rec.ID == id { // Assuming your struct has an "ID" field
				record = rec
				break
			}
		}

		if record.ID == 0 { // Record not found
			// Handle the error appropriately (e.g., redirect to error page)
			return
		}

		//get a reference to the persona
		person := "personas/" + (r.URL.Query().Get("person") + ".json")

		//retrieve preference data from file name matching person variable value
		preferenceData, err := os.ReadFile(person)
		if err != nil {
			fmt.Println("Error reading JSON file:", err)
			return
		}
		//unmarshal the preferenceData response into an UserPref struct
		var userpref UserPref
		err = json.Unmarshal(preferenceData, &userpref)
		if err != nil {
			fmt.Println("Error unmarshaling JSON:", err)
			return
		}

		//improve the message using Gemini
		ctx := context.Background()
		// Access your API key as an environment variable (see "Set up your API key" above)
		client, err := genai.NewClient(ctx, option.WithAPIKey(os.Getenv("GEMINI_API_KEY")))
		if err != nil {
			log.Fatal(err)
		}
		defer client.Close()

		// For text-only input, use the gemini-pro model
		model := client.GenerativeModel("gemini-pro")
		resp, err := model.GenerateContent(ctx, genai.Text(aiPremble+" "+userpref.Preferences+". "+record.Description))
		if err != nil {
			log.Fatal(err)
		}

		//parse the response from Gemini
		bs, _ := json.Marshal(resp.Candidates[0].Content.Parts[0])
		record.Description = string(bs)

		//execute the template, and pass in the record
		err = tmpl.ExecuteTemplate(w, "details.html", record)
		if err != nil {
			fmt.Println("Error executing template:", err)
		}
	})

	fmt.Println("Server listening on port 8080")
	fs := http.FileServer(http.Dir("./images"))
	http.Handle("/images/", http.StripPrefix("/images/", fs))
	http.ListenAndServe(":8080", nil)
}

I do not write great Go code, but it compiles, which is good enough for me!

On line 13, see that I refer to the Go package for interacting with the Gemini model. All you need is an API key, and we have a generous free tier.

On line 53, notice that I’m loading the data file based on the type of record picked on the HTML template.

On line 79, I’m executing the HTML template and sending the type of record (e.g. property, restaurant, store), the records themselves, and the persona.

On lines 108-113, I’m storing a map of prompt values to use for each type of record. These aren’t terrific, and could be written better to get smarter results, but it’ll do.

Notice on line 147 that I’m grabbing the user preferences we use for customization.

On line 163, I create a Gemini client so that I can interact with the LLM.

On line 171, see that I’m generating AI content based on the record-specific preamble, the record details, and the user preference data.

On line 177, notice that I’m extracting the payload from Gemini’s response.

Finally, on line 181 I’m executing the “details” template and passing in the AI-augmented record.

None of this is rocket science, and you can check out the whole project on GitHub.

What an “enterprise” version might look like

What I have here is a local example app. How would I make this more production grade?

  • Store catalog images in an object storage service. All my product images shouldn’t be local, of course. They belong in something like Google Cloud Storage.
  • Add catalog items and user preferences to a database. Likewise, JSON files aren’t a great database. The various items should all be in a relational database.
  • Write better prompts for the LLM. My prompts into Gemini are meh. You can run this yourself and see that I get some silly responses, like personalizing the message for a pillow by mentioning sporting events. In reality, I’d write smarter prompts that ensured the responding personalized item summary was entirely relevant.
  • Use Vertex AI APIs for accessing Gemini. Google AI Studio is terrific. For production scenarios, I’d use the Gemini models hosted in full-fledged MLOps platform like Vertex AI.
  • Run app in a proper cloud service. If I were really building this app, I’d host it in something like Google Cloud Run, or maybe GKE if it was part of a more complex set of components.
  • Explore whether pre-generating AI-augmented results and caching them would be more performant. It’s probably not realistic to call LLM endpoints on each “details” page. Maybe I’d pre-warm certain responses, or come up with other ways to not do everything on the fly.

This exercise helped me see the value of AI-assisted developer tooling firsthand. And, it feels like there’s something useful about LLM summarization being applied to a variety of “boring” app scenarios. What do you think?

Author: Richard Seroter

Richard Seroter is currently the Chief Evangelist at Google Cloud and leads the Developer Relations program. He’s also an instructor at Pluralsight, a frequent public speaker, the author of multiple books on software design and development, and a former InfoQ.com editor plus former 12-time Microsoft MVP for cloud. As Chief Evangelist at Google Cloud, Richard leads the team of developer advocates, developer engineers, outbound product managers, and technical writers who ensure that people find, use, and enjoy Google Cloud. Richard maintains a regularly updated blog on topics of architecture and solution design and can be found on Twitter as @rseroter.

3 thoughts

  1. Firstly, this is amazing, you are helping creators rethink the status quo of applications, and that’s the promise of AI-based applications. Sample code using Gemini AI is useful, and yes, you get the idea across well in the blog! To answer your question, though, as a Product Manager, it’s not my opinion but the framework to evaluate the idea that matters.

    The framework is (a) Does a market exist for such a product (b) Who’s the user and what are their top 3 pain point? (c)

    ❓ Market–No demand based on evidence that no tech company in personalization has reached critical mass. Salesforce acquired Evergage for ~$100M. This by itself doesn’t mean that websites (e-commerce websites) don’t solve the problem of personalization. Thousands of websites hire developers to write recommendation engines. This needs examination, and maybe there is a market out there for an AI-powered personalization engine for e-commerce. Sign me up to join your startup as the PM, and let’s get funding from GV 😀!

    ✅ User empathy–You are on the right track with marketplaces and the pain point. Users spend too much time finding information on any marketplace website. Think about how long it takes to find the restaurant you want to order takeout from, or to find the right product in the Amazon haystack, or the right person on LinkedIn. There’s a problem here that requires solving. Related to that is understanding retrieved information in a way that’s personalized to you.

    ✅ Pain point–Now, the problem you are solving is making the product you have already chosen relatable to you as a user. So if the user problem is–when information is presented, the website should show me what is relevant to me, then yes, that is a pain point. Think: when you look at a LinkedIn profile, the information that is important depends on whether the persona is a recruiter, hiring manager, partner, coworker, etc. Personalizing the LinkedIn profile would be amazing. Does this problem exist for any retail product? Well, let’s examine this further, I have opinions, as do you.

    ❓ Solution–If the problem is finding the right product, then the proposed solution doesn’t solve it. The solution assumes the user has found the right product already, and by then, they would have read the description already. So some iteration is required on the solution. Here’s an approach:

    💡 Refinement–If you could read the catalog and rewrite the descriptions based on the user’s preferences, demographic information, etc., that would be a better solution. Even better, filter the number of items returned based on your preferences. Now, we are writing a recommendation engine, and yes, I do think an LLM like Gemini can help. Now, when you click on an AI-enhanced profile, you are improving the experience for all users for search.

    Let’s build a better personalization engine using Google Gemini!

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.