|
import gradio as gr |
|
import cohere |
|
import os |
|
import re |
|
import json |
|
from langchain.prompts import ChatPromptTemplate |
|
from langchain_community.document_loaders import JSONLoader |
|
from langchain_huggingface import HuggingFaceEmbeddings |
|
from langchain_community.vectorstores import Chroma |
|
from langchain_cohere import ChatCohere |
|
from langchain_core.output_parsers import StrOutputParser |
|
from langchain_core.runnables import RunnableLambda, RunnablePassthrough |
|
|
|
|
|
embedding_function = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") |
|
|
|
persist_directory = "./chroma_langchain_db" |
|
|
|
vectordb_loaded = Chroma( |
|
persist_directory=persist_directory, |
|
embedding_function=embedding_function |
|
) |
|
|
|
def query_rag(query_text): |
|
results = vectordb_loaded.max_marginal_relevance_search(query=query_text, k=300, fetch_k=1500) |
|
context_text = "\n".join([doc.page_content for doc in results]) |
|
|
|
return context_text |
|
|
|
|
|
def collect_preferences(message, preferences): |
|
print(f"Current preferences before extraction: {preferences}") |
|
|
|
genre_names = [ |
|
"Point-and-click", |
|
"Fighting", |
|
"Shooter", |
|
"Music", |
|
"Platform", |
|
"Puzzle", |
|
"Racing", |
|
"Real Time Strategy (RTS)", |
|
"Role-playing (RPG)", |
|
"Simulator", |
|
"Sport", |
|
"Strategy", |
|
"Turn-based strategy (TBS)", |
|
"Tactical", |
|
"Hack and slash/Beat 'em up", |
|
"Quiz/Trivia", |
|
"Pinball", |
|
"Adventure", |
|
"Indie", |
|
"Arcade", |
|
"Visual Novel", |
|
"Card & Board Game", |
|
"MOBA" |
|
] |
|
|
|
game_modes = [ |
|
"Single player", |
|
"Multiplayer", |
|
"Co-operative", |
|
"Split screen", |
|
"Massively Multiplayer Online (MMO)" |
|
"Battle Royale" |
|
] |
|
|
|
extraction_prompt = f""" |
|
You are an expert assistant who manages user preferences about children's video games. |
|
|
|
Task: |
|
- You are given the CURRENT user preferences as a JSON dictionary. |
|
- You are also given a NEW user message. |
|
- Your job is to intelligently UPDATE the preferences based on the new message with the following lists: {genre_names}, {game_modes} |
|
- Update existing child entries if the information is about the same child. |
|
- Add new child entries if the user mentions a new child. |
|
- Merge new genres, game modes, and platforms with existing ones without duplication. |
|
- Respect any content to avoid preferences. |
|
- If something is no longer valid (e.g., user says "forget about racing games"), REMOVE that data. |
|
- If the user wants all preferences to be deleted or start over, then DELETE the CURRENT user preferences and CREATE a NEW empty one. |
|
- If the user input is vague (e.g., "fun", "educational"), only update the genre field with up to five related genres from the list. |
|
Do not guess game modes or platforms based on vague terms. Avoid inventing new children or unrelated preferences. |
|
- If the CURRENT user preferences is empty, then create the JSON dictionary like this: |
|
|
|
{{ |
|
"children": [ |
|
{{ |
|
"age": null, |
|
"genres": [], |
|
"game_modes": [], |
|
"platforms": [], |
|
"content_to_avoid": [] |
|
}}, |
|
}} |
|
|
|
AND then edit it based on the NEW user message. |
|
- Ignore the user message if that is not related to the topic. |
|
|
|
RULES: |
|
- Always respond with the FULL updated JSON, and ONLY the JSON. No extra text. |
|
- Preserve all data not mentioned in the new message unless explicitly removed. |
|
- Merge arrays without duplicates (e.g., genres, platforms). |
|
- If no changes are needed, simply output the original JSON. |
|
|
|
CURRENT USER PREFERENCES: |
|
{json.dumps(preferences, indent=2)} |
|
|
|
NEW USER MESSAGE: |
|
"{message}" |
|
""" |
|
|
|
extraction_response = client.chat( |
|
messages=[ |
|
{"role": "system", "content": extraction_prompt}, |
|
{"role": "user", "content": message} |
|
], |
|
model=COHERE_MODEL, |
|
) |
|
|
|
raw_output = extraction_response.message.content[0].text.strip() |
|
cleaned_output = re.sub(r"^```json\s*|\s*```$", "", raw_output).strip() |
|
|
|
try: |
|
updated_preferences = json.loads(cleaned_output) |
|
except json.JSONDecodeError: |
|
print(f"Error parsing extracted JSON: {raw_output}") |
|
updated_preferences = preferences |
|
|
|
print(f"Updated preferences after extraction: {updated_preferences}") |
|
return updated_preferences |
|
|
|
|
|
def missing_info(preferences): |
|
for child in preferences['children']: |
|
if child['age'] is None or not child['genres'] or not child['game_modes'] or not child['platforms']: |
|
return True |
|
|
|
return False |
|
|
|
COHERE_API_KEY = os.getenv("COHERE_API_KEY") |
|
client = cohere.ClientV2(COHERE_API_KEY) |
|
COHERE_MODEL = "command-a-03-2025" |
|
|
|
user_preferences = {} |
|
|
|
def respond(message, history): |
|
global user_preferences |
|
user_preferences = collect_preferences(message, user_preferences) |
|
|
|
if not missing_info(user_preferences): |
|
filtered = { |
|
'genres': [], |
|
'game_modes': [], |
|
'platforms': [] |
|
} |
|
|
|
for child in user_preferences['children']: |
|
filtered['genres'].extend(child.get('genres', [])) |
|
filtered['game_modes'].extend(child.get('game_modes', [])) |
|
filtered['platforms'].extend(child.get('platforms', [])) |
|
|
|
filtered = {k: list(set(v)) for k, v in filtered.items()} |
|
|
|
games = query_rag(str(filtered)) |
|
|
|
else: games = {} |
|
|
|
|
|
|
|
system_message = f""" |
|
You are a friendly and expert video game recommendation assistant helping parents find appropriate games for their children. |
|
|
|
Your job involves the following: |
|
|
|
1. You are given the user's extracted preferences in JSON format: |
|
{json.dumps(user_preferences, indent=2)} |
|
|
|
2. Check the data for completeness: |
|
- For each child, the following fields MUST be filled: |
|
- "age" must not be null. |
|
- "genres" must have at least one value. |
|
- "game_modes" must have at least one value. |
|
- "platforms" must have at least one value. |
|
- "content_to_avoid" can be empty, but should be present and also ask the user about this as well. |
|
|
|
3. If ANY required data is missing for any child: |
|
- DO NOT suggest games yet. |
|
- ONLY ask for the missing details, not the ones already filled in. |
|
- Be polite and encouraging, acknowledging what is already known. |
|
- Be specific and list *only* what is missing per child. |
|
- Wait for the user's response before continuing. |
|
- If the user input was vague (see here:{message}), then make sure to tell them how it was interpreted relating to the genres. |
|
|
|
4. Only AFTER all required data is complete for all children: |
|
- Use the context below to find matching games. |
|
- DO NOT invent games or use outside knowledge. |
|
- Only recommend games from this context: |
|
{games} |
|
|
|
5. For each child, recommend exactly five games that: |
|
- Match their genre, platform, and game mode preferences. |
|
- Are appropriate for their age. |
|
- Do NOT contain any content the parent wants to avoid. |
|
|
|
6. If a requested genre is unsuitable for the child's age, offer safer alternatives with an explanation. |
|
|
|
7. NEVER recommend the same game more than once in a session. |
|
|
|
8. When ready, use the following format for each game: |
|
|
|
Game 1: |
|
- Name: [Title of the game from the context] |
|
- Genres: [List of all genres from the context separated by commas word for word] |
|
- Themes: [List of all themes from the context separated by commas word for word, if any] |
|
- Age Ratings: [List of all age ratings from the context separated by commas word for word] |
|
- Game Modes: [List of all game modes from the context separated by commas word for word] |
|
- Platforms: [List of all platforms from the context on which the game is available separated by commas word for word] |
|
- Summary: [The summary of the game word for word from the context] |
|
- Reasons for recommendation |
|
|
|
9. After recommending, ask the user: |
|
- If they're satisfied. |
|
- If they'd like to update preferences or receive more recommendations. |
|
- If they have any questions about the recommended games. |
|
|
|
Only proceed to recommendations when you are 100% certain all required data is present and valid. |
|
|
|
Stay on the topic of videogame recommendation, remind the user about this as well if their inputs are not related to the topic. |
|
""" |
|
|
|
messages = [{"role": "system", "content": system_message}] |
|
|
|
for val in history: |
|
if val[0]: |
|
messages.append({"role": "user", "content": val[0]}) |
|
if val[1]: |
|
messages.append({"role": "assistant", "content": val[1]}) |
|
|
|
messages.append({"role": "user", "content": message}) |
|
|
|
response = client.chat( |
|
messages=messages, |
|
model=COHERE_MODEL, |
|
) |
|
|
|
text_output = response.message.content[0].text |
|
|
|
return text_output |
|
|
|
|
|
demo = gr.ChatInterface( |
|
respond, |
|
chatbot=gr.Chatbot(value=[[None, |
|
""" |
|
Hi there! I'm here to help you find the perfect video games for your child. To get started, could you please provide the following information: |
|
|
|
1. Age of the child: This will help ensure the games are age-appropriate. |
|
2. Preferred genres: What types of games do they enjoy? (e.g., adventure, sports, puzzle) |
|
3. Game mode preferences: Do they prefer single-player, multiplayer, or both? |
|
4. Platforms: Which devices do you have? (e.g., PC, PlayStation, Xbox, Nintendo Switch) |
|
5. Content to avoid: Are there any themes or contents you'd like to avoid? (e.g., violence, horror) |
|
|
|
If there are multiple children, please provide details for each. |
|
Once I have all the information, I make sure to store it for future use and I’ll suggest five suitable games for each child! |
|
|
|
You can change your preferences at any time, I'll make sure to keep up with you! |
|
You can even request to start over the whole information gathering process! |
|
If you want me to delete all the stored information that I have about your preferences, then I'll happily comply! |
|
""" |
|
]]), |
|
title="Videogame Recommender Chatbot", |
|
description=""" |
|
Welcome dear Tester! |
|
This chatbot is designed to help parents and guardians find suitable and age-appropriate video games for their children. |
|
It considers the following factors: age, preferred genres, game mode preferences, available platforms and contents to avoid. |
|
|
|
Your Role as a Tester: |
|
Please try using the chatbot as if you're a parent or guardian searching for a new game for your child. |
|
I advise you to do the testing in multiple recommendation sessions for more results! |
|
While testing check out the survey below and test the chatbot based on the questions found there! |
|
To check if the chatbot gave accurate info, use https://www.igdb.com/ or search inside the "games.json" file found in the "Files" menu. |
|
After interacting with the chatbot, fill out the short survey to help me evaluate its usefulness, clarity, and ease of use. Your feedback is very important! |
|
|
|
The survey is available here: https://docs.google.com/forms/d/e/1FAIpQLSdyQ3LPr3UCf4JA6sbs4tcqsA5Bxdark5hS4QvIawFthChqBA/viewform?usp=sharing |
|
|
|
IF YOU DON'T SEE THE FULL MESSAGE ABOVE THEN PRESS THE "F11" KEY TWICE! |
|
""", |
|
autoscroll=True, |
|
) |
|
|
|
if __name__ == "__main__": |
|
demo.launch(ssr_mode=False) |