Logo

AI Product Research Agent

AI powered voice assistant that researches gifts with an LLM brain

Agent Santa

LLM Santa is an AI-powered voice assistant that researches gifts with an LLM brain. Writes 5 researched and recommended gift ideas to a google spreadsheet.

Video

Prerequisites

Before starting, you'll need the following accounts and API keys:

  • Google Drive: (link)
    • To write the output spreadsheet
  • Make.com: (link)
    • To create a automation workflow
    • Free tier available
  • SERP API: (link)
    • To generate search results
    • Free tier available
  • SystemPrompt.io: (link)
    • To convert messages into structured data
    • Free 14 day trial available (must choose execution plan of $30/month)

Instructions

Step 1: Record a voice message

a. Use SpeechRecognitionAPI to record a voice message

The first step it to record a voice message. For this we will use the SpeechRecognitionAPI. Learn more.

File: SantaWrapper.tsx

"use client";
import Santa from "./Santa";
import { useState, useRef } from "react";
import "./santa.css";
 
export default function AiSantaClient() {
  const [isTalking, setIsTalking] = useState(false);
  const [isRecording, setIsRecording] = useState(false);
  const [isButtonDisabled, setIsButtonDisabled] = useState(false);
  const recognitionRef = useRef<SpeechRecognition | null>(null);
  const transcriptRef = useRef<string>("");
  const webhookUrl = "https://hook.eu2.make.com/wdfuvmtnlo1ra8fjwlw63ealybueyln9";
 
  const handleToggleTalking = () => {
    if (!isTalking) {
      // Check for browser support
      const SpeechRecognition =
        (window as any).SpeechRecognition ||
        (window as any).webkitSpeechRecognition;
 
      if (!SpeechRecognition) {
        console.error(
          "SpeechRecognition API is not supported in this browser."
        );
        return;
      }
 
      recognitionRef.current = new SpeechRecognition();
      recognitionRef.current.continuous = false;
      recognitionRef.current.interimResults = false;
      recognitionRef.current.lang = "en-US";
 
      recognitionRef.current.onresult = async (event) => {
        const transcript = event.results[0][0].transcript;
        transcriptRef.current = transcript;
 
        try {
          setIsButtonDisabled(true);
          const response = await fetch(webhookUrl, {
            method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({ transcript }),
            }
          );
 
          if (!response.ok) {
            throw new Error("Failed to send transcript");
          }
          console.log("Transcript sent successfully to the webhook.");
        } catch (error) {
          console.error("Error sending transcript:", error);
        } finally {
          setIsButtonDisabled(false);
        }
      };
 
      recognitionRef.current.onerror = (event) => {
        console.error("Speech recognition error:", event.error);
      };
 
      recognitionRef.current.onend = () => {
        setIsRecording(false);
        setIsTalking(false);
      };
 
      recognitionRef.current.start();
      setIsRecording(true);
      setIsTalking(true);
    } else {
      if (recognitionRef.current && isRecording) {
        recognitionRef.current.stop();
        setIsRecording(false);
      }
      setIsTalking(false);
    }
  };
 
  return (
    <main
      className={`${styles["santa-wrapper"]} ${isTalking ? "talking" : ""}`}
    >
      <Santa isTalking={isTalking} />
      <div className={styles.actionBar}>
        <p className="instructions">
          Let Santa help you find the perfect present for Christmas
        </p>
        <button
          onClick={handleToggleTalking}
          className={styles.toggleButton}
          disabled={isButtonDisabled}
        >
          {isTalking ? "Stop Santa Talking" : "Toggle Santa Talking"}
        </button>
      </div>
    </main>
  );

b. [optional] Create an illustration of Santa

If you want to add a sparkling of Santa, you can use the following code to create an illustration of Santa. (credit)

The code will be fully functional without this step, as all functionality is in the SantaWrapper.tsx file.

File: Santa.tsx

import styles from "./santa.css";
 
interface SantaProps {
  isTalking?: boolean;
}
 
export default function Santa({ isTalking = false }: SantaProps) {
  return (
    <div
      className={`${styles.cartoon} ${styles.hb} ${isTalking ? styles.talking : ""}`}
    >
      <div className={`${styles.leg} ${styles.ha}`}></div>
      <div className={`${styles.leg} ${styles.ha}`}></div>
      <div className={`${styles.hands} ${styles.r}`}></div>
      <div className={styles.arm}></div>
      <div className={styles.body}>
        <div className={styles.belt}></div>
        <div className={`${styles.buttons} ${styles.r}`}></div>
      </div>
      <div className={styles.beard}></div>
      <div className={`${styles.head} ${styles.r}`}></div>
      <div className={styles.mustache}></div>
      <div className={styles.mustache}></div>
      <div className={`${styles.cheeks} ${styles.r}`}></div>
      <div className={`${styles.eyes} ${styles.r}`}></div>
      <div className={`${styles.hat} ${styles.ha} ${styles.hb}`}></div>
    </div>
  );
}

file: Santa.css

.cartoon {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 80vmin;
  height: 80vmin;
}
 
.cartoon div {
  position: absolute;
  box-sizing: border-box;
}
 
.b {
  border: 0.5vmin solid;
}
 
.r {
  border-radius: 100%;
}
 
.hb::before,
.ha::after {
  content: "";
  display: block;
  position: absolute;
  box-sizing: border-box;
}
 
/****/
@keyframes snow {
  0% {
    background-position:
      0 0,
      0 0,
      0 0,
      0 0;
  }
  40% {
    background-position:
      10px 14vmin,
      -20px 28vmin,
      20px 20vmin,
      75px 20vmin;
  }
  60% {
    background-position:
      -10px 21vmin,
      -30px 42vmin,
      30px 30vmin,
      50px 30vmin;
  }
  100% {
    background-position:
      0 35vmin,
      0 70vmin,
      0 50vmin,
      0 50vmin;
  }
}
 
.santa-wrapper {
  margin: 0;
  padding: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: #1585;
  background-image: radial-gradient(
      circle at 50% 50%,
      white 2.5%,
      transparent 0
    ),
    radial-gradient(circle at 30% 90%, white 1.5%, transparent 0),
    radial-gradient(circle at 70% 10%, white 1%, transparent 0),
    radial-gradient(circle at 10% 40%, white 1%, transparent 0);
  background-size:
    45vmin 35vmin,
    50vmin 70vmin,
    60vmin 50vmin,
    65vmin 50vmin;
  background-position:
    0 0,
    0 0,
    0 0,
    0 0;
}
 
.cartoon {
  --skin: #fca;
  --beard: #eee;
  --eyes: #630a;
  --cheeks: #f001;
  --belt: #111;
  --belt-buckle: gold;
  --suit: #a00;
}
 
.head {
  width: 25%;
  height: 25%;
  background: var(--skin);
  top: 10%;
  left: 50%;
  transform: translate(-50%, 0);
}
 
.beard {
  width: 30%;
  height: 40%;
  background: var(--beard);
  top: 10%;
  left: 50%;
  transform: translate(-50%, 0);
  border-radius: 100% / 120% 120% 80% 80%;
}
 
.mustache {
  width: 10%;
  height: 10%;
  background: #fff;
  border-radius: 100% 20% 100% 0;
  top: 31%;
  left: 51%;
  transform-origin: top right;
  transform: translate(-100%, 0) rotate(25deg);
}
 
.mustache + .mustache {
  left: 49%;
  border-radius: 20% 100% 0 100%;
  transform-origin: top left;
  transform: rotate(-25deg);
}
 
.eyes {
  width: 2%;
  height: 2%;
  background: var(--eyes);
  top: 23%;
  left: 45%;
  box-shadow: 6.66vmin 0 var(--eyes);
}
 
.cheeks {
  width: 5%;
  height: 3%;
  background: var(--cheeks);
  top: 25.5%;
  left: 43%;
  box-shadow: 7.25vmin 0 var(--cheeks);
}
 
.body {
  width: 50%;
  height: 50%;
  background: var(--suit);
  border-radius: 100% / 150% 150% 25% 25%;
  top: 35%;
  left: 50%;
  transform: translate(-50%, 0);
  background-image: radial-gradient(
      circle at 50% -50%,
      transparent 75%,
      var(--belt) 0 83%,
      transparent 0
    ),
    linear-gradient(to right, transparent 42%, white 43% 57%, transparent 58%);
}
 
.arm {
  width: 65%;
  height: 40%;
  background: #a00;
  border-radius: 100% / 170% 170% 25% 25%;
  top: 37%;
  left: 50%;
  transform: translate(-50%, 0);
  box-shadow: inset 0 0 0 10vmin #0002;
  background-image: linear-gradient(transparent 20%, #0003);
}
 
.belt {
  width: 20%;
  height: 15%;
  border: 1vmin solid var(--belt-buckle);
  border-radius: 1vmin;
  top: 75%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--belt-buckle);
  box-shadow: inset 1vmin 0 0 1.75vmin var(--belt);
}
 
.buttons {
  width: 5%;
  height: 5%;
  background: var(--belt);
  color: var(--belt);
  top: 33%;
  left: 50%;
  transform: translate(-50%, 0);
  box-shadow:
    0 5vmin,
    0 10vmin 0 0.1vmin,
    0 22vmin;
  opacity: 0.75;
}
 
.hat {
  width: 23%;
  height: 20%;
  background: var(--suit);
  border-radius: 100% 20% 0 0;
  top: -2%;
  left: 50%;
  transform: translate(-50%, 0) rotate(1deg);
}
 
.hat::before {
  width: 110%;
  height: 40%;
  border-radius: 100% / 50%;
  bottom: -17%;
  left: -5%;
  box-shadow: inset 0 4vmin white;
  transform: rotate(-2deg);
}
 
.hat::after {
  width: 8vmin;
  height: 8vmin;
  border-radius: 50%;
  background: var(--beard);
  right: -5vmin;
  top: -15%;
}
 
.hands {
  width: 13%;
  height: 13%;
  background: var(--belt);
  top: 70%;
  left: 18%;
  box-shadow: 41vmin 0 var(--belt);
}
 
.leg {
  width: 19%;
  height: 25%;
  background: var(--suit);
  transform: skew(2deg);
  top: 75%;
  left: 29%;
  background-image: linear-gradient(#0002, transparent 70%, var(--belt) 0);
}
 
.leg + .leg {
  left: 52%;
}
 
.leg::after {
  width: 110%;
  height: 20%;
  background: black;
  bottom: 0;
  left: -6%;
  border-radius: 10vmin 10vmin 0 0;
}
 
.leg + .leg::after {
  left: -4%;
}
 
/* Add these new animations for talking */
@keyframes talking {
  0%,
  100% {
    transform: translate(-50%, 0);
  }
  50% {
    transform: translate(-50%, 2%);
  }
}
 
.talking .beard {
  animation: talking 0.5s infinite ease-in-out;
}
 
.talking .mustache {
  animation: talking 0.5s infinite ease-in-out;
}
 
/* Toggle Button Styles */
.toggleButton {
  padding: 0.75rem 1.5rem;
  font-size: 1.2rem;
  background-color: #ff4757;
  color: white;
  border: none;
  border-radius: 50px;
  cursor: pointer;
  transition:
    background-color 0.3s ease,
    box-shadow 0.3s ease,
    transform 0.2s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 200px;
  height: 50px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
 
.toggleButton:hover {
  background-color: #e84118;
  box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
  transform: translateY(-2px);
}
 
.toggleButton:active {
  background-color: #c23616;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transform: translateY(0);
}
 
.toggleButton:disabled {
  background-color: #a4b0be;
  cursor: not-allowed;
  box-shadow: none;
}
 
/* Action Bar Styles */
.actionBar {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  background: rgba(255, 255, 255, 0.95);
  padding: 15px 30px;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 20px;
}
 
.speechBubble {
  background: #ffffff;
  border-radius: 15px;
  padding: 20px;
  max-width: 60%;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  font-family: "Comic Sans MS", cursive, sans-serif;
  font-size: 1rem;
  color: #333333;
  position: relative;
  animation: fadeIn 0.5s ease-in-out;
}
 
.speechBubble::after {
  content: "";
  position: absolute;
  top: 50%;
  left: -15px; /* Position the tail towards Santa */
  transform: translateY(-50%);
  width: 0;
  height: 0;
  border: 15px solid transparent;
  border-right-color: #ffffff;
}
 
.inputField {
  display: block;
  margin: 10px auto;
  padding: 10px 15px;
  width: 90%;
  max-width: 250px;
  border: 2px solid #ff6f61;
  border-radius: 25px;
  font-size: 1rem;
  transition: border-color 0.3s ease;
}
 
.inputField:focus {
  border-color: #ff3b2e;
  outline: none;
  box-shadow: 0 0 5px rgba(255, 59, 46, 0.5);
}
 
/* Fade-in Animation for Speech Bubble */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
 
/* Responsive Adjustments */
@media (max-width: 768px) {
  .actionBar {
    flex-direction: column;
    padding: 10px 20px;
    gap: 15px;
  }
 
  .speechBubble {
    max-width: 80%;
  }
 
  .toggleButton {
    min-width: 150px;
    height: 45px;
    font-size: 1rem;
  }
}
 
@media (max-width: 480px) {
  .speechBubble {
    max-width: 100%;
  }
 
  .toggleButton {
    width: 100%;
  }
}

c. Create a webhook URL

We now need to create a webhook URL that will be used to post the text translation of the voice message to. When we click the toggle button, we will post the text translation of the voice message to the webhook URL.

Go to your make.com account and create a new webhook URL in a new scenario called AI Santa. Copy the webhook URL and paste it into the WEBHOOK_URL variable in the SantaWrapper.tsx file.

d. Post the text translation to the Webhook

Now we are ready to test the scenario. Click the toggle button and see if Santa can interpret your voice message.

You have now successfully created a voice-activated Santa that can interpret your voice message and write it to a google spreadsheet.

  1. The button in the React component should listen to your microphone
  2. The VoiceRecognitionAPI will record your message and convert it to text
  3. The text is then posted to the webhook URL
  4. You can see your text in the output bundle in the Make.com webhook scenario
  5. See it in action here: LLM Santa
Webhook listening to santa

Step 2: Interprete the voice message

a. Create a prompt

We need to create a prompt that can interpret the voice message and convert it to a structured query. We can do this via API using the command below from the terminal. We could also use the Systemprompt.io Wizard prompt wizard.

curl -X POST "https://api.systemprompt.io/v1/prompt" \
  -H "api-key: <apikey>" \
  -H "Content-Type: application/json" \
  -d '{
    "instruction": {
      "static": "1. Assume the role of Santa who understands the needs and desires of individuals from the information provided.\n2. Receive detailed information about a user, which can include age, interests, hobbies, recent activities, preferences, and any known wishlist items.\n3. Use this information to craft a Google search query that would help find a Christmas present suitable for the user.\n4. The search query should be concise but include key terms likely to yield relevant and desirable gift options.\n5. Consider various types of presents such as toys, gadgets, experiences, fashion items, or books, based on the user'\''s information.\n6. Maintain a warm and jolly tone in crafting the response, ensuring it embodies the spirit of Santa Claus.\n7. Provide a justification for the query choices if necessary, explaining how they align with the user'\''s interests or personality traits.\n8. Be adaptable to unusual or sparse user information, utilizing general festive knowledge or common gift-giving trends when specific details are lacking.",
      "state": "{{conversation.history}}",
      "dynamic": "{{message}}"
    },
    "input": {
      "name": "SantaGiftQuerySchema",
      "description": "This schema outlines instructions for generating a search query as Santa Claus to find an ideal Christmas gift. It includes guidelines for understanding user information, utilizing it to craft meaningful queries, and maintaining a festive tone. The goal is to leverage user data like age, interests, and preferences to form effective search terms that identify suitable gift ideas online, embodied by the spirit of Santa.",
      "type": ["message"],
      "reference": []
    },
    "output": {
      "name": "SantaGiftQuerySchema",
      "type": "structured_data",
      "schema": {
        "type": "object",
        "required": [
          "queryString",
          "language",
          "region"
        ],
        "properties": {
          "region": {
            "type": "string",
            "description": "The regional setting for the search, specified as a country code."
          },
          "language": {
            "type": "string",
            "description": "The language in which the search results are desired."
          },
          "queryString": {
            "type": "string",
            "description": "The main text of the search query."
          }
        },
        "additionalProperties": false
      }
    },
    "metadata": {
      "title": "Santa",
      "description": "You are santa, given information about a user, create the perfect search query for finding a christmas present in google."
    }
  }'

b. Edit the prompt

After creating the prompt, we can edit it in the systemprompt console. Take note of the title, as we will need this to call the prompt via the API.

** Important: **

Key things to note here, the output type is structured_data and the schema is defined in the output section, this is important for mapping in the Make.com scenario. We've provided a sample schema, but you can edit this to be more specific.

The instructions will be posted to the LLM. The state variable is optional, but can be used to include the conversation history. The dynamic variable is required, and will be replaced by the text translation of the voice message.

c. Add systemprompt API call to Make.com scenario

We now need to add the systemprompt API call to the Make.com scenario.

Select execute prompt from the Systemprompt API integration. Select the prompt title we created earlier.

We need to select the output of the webhook "transcript" as the input for the prompt.

d. Success

Santa AI Voice

Step 3: Use SERP API to generate search results

a. Map the search query

We can add the SERP API call to the Make.com scenario. Select the search query from the previous step as the input.

Santa AI Voice

b. JSON parsing

We can now parse the JSON response from the SERP API call. To do this, select the output bundle from the SERP API call, and copy and paste this into the data structure of the JSON parser. Make sure to map the output correctly.

Santa AI Voice

Step 4: Convert the structured query into gift ideas

Make sure you are testing the scenario as you progress, you should now be able to talk to Santa and get a list a products from the SERP API. The next step it to analyse and parse the results into a list of gift ideas.

a. Create a prompt

We need to create a prompt that can interpret the structured query and convert it to a list of gift ideas. We can do this via API using the command below from the terminal. We could also use the Systemprompt.io Wizard prompt wizard.

curl -X POST "https://api.systemprompt.io/v1/prompt" \
  -H "api-key: <apikey>" \
  -H "Content-Type: application/json" \
  -d '{
    "instruction": {
      "static": "As the SantaProductSelector, your task is to recommend five suitable Christmas gifts from a given list of products, tailored to a specific gift recipient. Use the following guidelines: \n\n1. Maintain a warm and festive tone\n2. Analyze the product list thoroughly\n3. Understand the gift recipient description\n4. Match products to recipient traits\n5. Balance practical and fun items\n6. Optionally rank or explain choices\n7. Focus on spreading festive joy",
      "state": "{{conversation.history}}",
      "dynamic": "{{message}}"
    },
    "input": {
      "name": "SantaProductSelector",
      "description": "The SantaProductSelector schema is designed for identifying potential Christmas gifts. It processes a list of products and recipient details to provide tailored gift recommendations. It emphasizes understanding the recipient's preferences and selecting a diverse set of gifts, balancing practicality and entertainment. The output includes a ranked list of five gifts with possible explanations to highlight reasoning. Input should be clearly categorized into 'Product List' and 'Recipient Description' for effective processing.",
      "type": ["message"],
      "reference": []
    },
    "output": {
      "name": "SantaProductSelector",
      "type": "structured_data",
      "schema": {
        "type": "object",
        "properties": {
          "products": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "productLink",
                "title",
                "description",
                "price",
                "choiceMessage"
              ],
              "properties": {
                "price": {
                  "type": "number",
                  "description": "The price of the product."
                },
                "title": {
                  "type": "string",
                  "description": "The title of the product."
                },
                "description": {
                  "type": "string",
                  "description": "A brief description of the product."
                },
                "productLink": {
                  "type": "string",
                  "format": "uri",
                  "description": "A URL link to the product page."
                },
                "choiceMessage": {
                  "type": "string",
                  "description": "A message explaining why this product was chosen."
                }
              }
            },
            "maxItems": 5,
            "minItems": 5
          }
        },
        "required": ["products"]
      }
    },
    "metadata": {
      "title": "Santa product picker",
      "description": "Given a list of products and a description of the person to be gifted to, pick 5 potential gifts for christmas"
    }
  }'

b. Edit the prompt

After creating the prompt, we can edit it in the systemprompt console. Take note of the title, as we will need this to call the prompt via the API.

** Important: **

Key things to note here, the output type is structured_data and the schema is defined in the output section, this is important for mapping in the Make.com scenario. We've provided a sample schema, but you can edit this to be more specific.

c. Add systemprompt API call to Make.com scenario

We now need to add the systemprompt API call to the Make.com scenario.

Select execute prompt from the Systemprompt API integration. Select the prompt title we created earlier.

We need to select the output of the JSON parser as the input for the prompt.

d. Success

Santa AI Voice

Step 5: Write gift ideas to a google spreadsheet

We are nearly finished now. We can now write the gift ideas to a google spreadsheet.

a. Create an iterator

Select the output of the structured data prompt as the input for the iterator.

Santa AI Voice

b. Write to spreadsheet

Add a google spreadsheet integration and select the iterator as the input.

Santa AI Voice

c. Map the values

Make sure you make the values of the structured data from systemprompt to the columns of the google spreadsheet.

Resources

Make.com Template

Troubleshooting

Any problems following the demo? Reach out to us on Discord and we'll help you out.

On this page