From ece25b29e5c6fe685f08281ff6ba8773b875ca97 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Thu, 30 Jan 2025 12:20:42 +0100 Subject: [PATCH 01/11] update allow_origin to static page hugginface space --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 96ca86d..381417d 100644 --- a/app/main.py +++ b/app/main.py @@ -40,7 +40,7 @@ # app.add_middleware( CORSMiddleware, - allow_origins=["*"], # "https://your-custom-domain.com" if credentials are needed + allow_origins=["https://mistral-ai-game-jam-neuraljam.static.hf.space"], allow_credentials=False, # Must be False if allow_origins=["*"] in modern browsers allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"], allow_headers=["*"], # You could limit this to ["Accept", "X-Access-Token", ...] if desired From beac7869a56f499ab5ac042f71e168c46f3f72f2 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Thu, 30 Jan 2025 13:52:42 +0100 Subject: [PATCH 02/11] add train generation endpoint --- app/main.py | 4 +- app/models/train.py | 46 +++++ app/routes/generate.py | 50 ++++++ app/services/generate_train/__init__.py | 0 app/services/generate_train/convert.py | 161 +++++++++++++++++ app/services/generate_train/generate_train.py | 169 ++++++++++++++++++ 6 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 app/models/train.py create mode 100644 app/routes/generate.py create mode 100644 app/services/generate_train/__init__.py create mode 100644 app/services/generate_train/convert.py create mode 100644 app/services/generate_train/generate_train.py diff --git a/app/main.py b/app/main.py index 381417d..8fe2872 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,6 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import Response -from app.routes import health, wagons, chat, players +from app.routes import health, wagons, chat, players, generate from app.core.logging import get_logger from dotenv import load_dotenv from datetime import datetime @@ -155,6 +154,7 @@ async def log_requests(request: Request, call_next): app.include_router(wagons.router) app.include_router(chat.router) app.include_router(players.router) +app.include_router(generate.router) # ----------------------------------------------------------------------------- # 6. Basic root endpoint diff --git a/app/models/train.py b/app/models/train.py new file mode 100644 index 0000000..ac67ba2 --- /dev/null +++ b/app/models/train.py @@ -0,0 +1,46 @@ +from pydantic import BaseModel, Field +from typing import List, Dict + +class PassengerProfile(BaseModel): + name: str + age: int + profession: str + personality: str + role: str + mystery_intrigue: str + +class PlayerName(BaseModel): + firstName: str + lastName: str + sex: str + fullName: str + +class PlayerDetails(BaseModel): + profile: PassengerProfile + +class Person(BaseModel): + uid: str + position: List[float] = Field(..., min_items=2, max_items=2) + rotation: float + model_type: str + items: List[str] = [] + +class Wagon(BaseModel): + id: int + theme: str + passcode: str + people: List[Person] + +class Names(BaseModel): + names: Dict[str, Dict[str, PlayerName]] + +class PlayerDetailsResponse(BaseModel): + player_details: Dict[str, Dict[str, PlayerDetails]] + +class WagonsResponse(BaseModel): + wagons: List[Wagon] + +class GenerateTrainResponse(BaseModel): + names: Names + player_details: PlayerDetailsResponse + wagons: WagonsResponse \ No newline at end of file diff --git a/app/routes/generate.py b/app/routes/generate.py new file mode 100644 index 0000000..0d849b2 --- /dev/null +++ b/app/routes/generate.py @@ -0,0 +1,50 @@ +from fastapi import APIRouter, HTTPException, Depends +from app.services.session_service import SessionService +from app.models.session import UserSession +from app.services.generate_train.generate_train import generate_train +from app.models.train import GenerateTrainResponse, Names, PlayerDetailsResponse, WagonsResponse + +router = APIRouter( + prefix="/api/generate", + tags=["chat"] +) + +@router.get("/train/{session_id}/{number_of_wagons}/{theme}", response_model=GenerateTrainResponse) +async def get_generated_train(session_id: str, number_of_wagons: str, theme: str): + # session = SessionService.get_session(session_id) + # if not session: + # raise HTTPException(status_code=404, detail="Session not found") + + # Convert and validate number_of_wagons + try: + number_of_wagons = int(number_of_wagons) + except ValueError: + raise HTTPException( + status_code=400, + detail="number_of_wagons must be an integer" + ) + + if number_of_wagons <= 0: + raise HTTPException( + status_code=400, + detail="number_of_wagons must be greater than 0" + ) + + if number_of_wagons > 6: + raise HTTPException( + status_code=400, + detail="number_of_wagons cannot exceed 6" + ) + + # Generate train data using the existing function + names, player_details, wagons = generate_train(theme, number_of_wagons) + + # Create Pydantic models from the response + response = GenerateTrainResponse( + names=Names(names=names["names"]), + player_details=PlayerDetailsResponse(player_details=player_details["player_details"]), + wagons=WagonsResponse(wagons=wagons["wagons"]) + ) + + return response + \ No newline at end of file diff --git a/app/services/generate_train/__init__.py b/app/services/generate_train/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/generate_train/convert.py b/app/services/generate_train/convert.py new file mode 100644 index 0000000..3d8969d --- /dev/null +++ b/app/services/generate_train/convert.py @@ -0,0 +1,161 @@ +import random + +def parse_name(full_name): + """ + Splits the full name into firstName and lastName. + This is a simple heuristic: + - lastName is the last token + - firstName is everything before + For example: + "Dr. Amelia Hartford" -> firstName: "Dr. Amelia", lastName: "Hartford" + "Thomas Maxwell" -> firstName: "Thomas", lastName: "Maxwell" + Adjust this to your own naming conventions if needed. + """ + tokens = full_name.strip().split() + if len(tokens) == 1: + return full_name, "" # No clear lastName + else: + return " ".join(tokens[:-1]), tokens[-1] + +def infer_sex_from_model(model_type): + """ + A simple inference: if the string 'female' is in model_type, mark sex as 'female'; + if 'male' is in model_type, mark as 'male'; + otherwise, mark as 'unknown' (or handle however you prefer). + """ + model_type_lower = model_type.lower() + if 'female' in model_type_lower: + return 'female' + elif 'male' in model_type_lower: + return 'male' + else: + return 'unknown' + +def convert_wagon_to_three_jsons(wagon_data): + """ + Given a single wagon JSON structure like: + { + "id": 1, + "theme": "Alien by Ridley Scott", + "passcode": "Nostromo", + "passengers": [ + { + "name": "Dr. Amelia Hartford", + "age": 47, + "profession": "Medical Researcher", + "personality": "Analytical, compassionate, and meticulous", + "role": "...", + "mystery_intrigue": "...", + "characer_model": "character-female-e" + }, + ... + ] + } + produce: + 1) names_json + 2) player_details_json + 3) wagons_json + """ + wagon_id = wagon_data.get("id", 0) + theme = wagon_data.get("theme", "Unknown Theme") + passcode = wagon_data.get("passcode", "no-passcode") + passengers = wagon_data.get("passengers", []) + + # 1) Build the "names" object for this wagon + # The final structure should be: {"wagon-N": {"player-1": {...}, "player-2": {...}, ...}} + names_output = {} + wagon_key = f"wagon-{wagon_id}" + names_output[wagon_key] = {} + + # 2) Build the "player_details" object for this wagon + # The final structure: {"wagon-N": {"player-1": { "profile": {...}}, "player-2": {"profile": {...}}}} + player_details_output = {} + player_details_output[wagon_key] = {} + + # 3) Build the "wagons" array entry for this wagon + # Each wagon in the final output is something like: + # { + # "id": wagon_id, + # "theme": "...", + # "passcode": "...", + # "people": [ + # { + # "uid": "wagon-N-player-i", + # "position": [rand, rand], + # "rotation": rand, + # "model_type": "character-female-e", + # "items": [] + # }, ... + # ] + # } + wagon_entry = { + "id": wagon_id, + "theme": theme, + "passcode": passcode, + "people": [] + } + + # Loop over passengers to fill each part + for i, passenger in enumerate(passengers, start=1): + player_key = f"player-{i}" + full_name = passenger.get("name", "Unknown") + first_name, last_name = parse_name(full_name) + + model_type = passenger.get("characer_model", "character-unknown") + sex = infer_sex_from_model(model_type) + + # 1) Fill names + names_output[wagon_key][player_key] = { + "firstName": first_name, + "lastName": last_name, + "sex": sex, + "fullName": full_name + } + + # 2) Fill player_details + profile_dict = { + "name": full_name, + "age": passenger.get("age", 0), + "profession": passenger.get("profession", ""), + "personality": passenger.get("personality", ""), + "role": passenger.get("role", ""), + "mystery_intrigue": passenger.get("mystery_intrigue", "") + } + player_details_output[wagon_key][player_key] = { + "profile": profile_dict + } + + # 3) Fill wagons "people" + # Random position within [0..1], random rotation in [0..1], items always [] + person_dict = { + "uid": f"wagon-{wagon_id}-player-{i}", + "position": [round(random.random(), 2), round(random.random(), 2)], + "rotation": round(random.random(), 2), + "model_type": model_type, + "items": [] + } + wagon_entry["people"].append(person_dict) + + return names_output, player_details_output, wagon_entry + +def convert_and_return_jsons(wagon_data): + names_result, player_details_result, wagons_entry = {}, {}, [] + for wagon in wagon_data: + names_output, player_details_output, wagon_entry = convert_wagon_to_three_jsons(wagon) + names_result.update(names_output) + player_details_result.update(player_details_output) + wagons_entry.append(wagon_entry) + + # 1) The 'names' JSON typically might aggregate multiple wagons, so we embed our single wagon's result: + # For demonstration, just put it as "names": { ...single wagon data... } + all_names = {"names": names_result} + + # 2) The 'player_details' JSON also might aggregate multiple wagons + all_player_details = {"player_details": player_details_result} + + # 3) The 'wagons' JSON is typically an array of wagons. Here, we only have one: + all_wagons = { + "wagons": wagons_entry + } + + return all_names, all_player_details, all_wagons \ No newline at end of file diff --git a/app/services/generate_train/generate_train.py b/app/services/generate_train/generate_train.py new file mode 100644 index 0000000..6fb34b1 --- /dev/null +++ b/app/services/generate_train/generate_train.py @@ -0,0 +1,169 @@ +from mistralai import Mistral +import os +import json +import random + +from app.services.generate_train.convert import convert_and_return_jsons + +# Get the Mistral API key from the .env file +api_key = os.getenv("MISTRAL_API_KEY") + +if not api_key: + raise ValueError("MISTRAL_API_KEY is not set in the .env file.") + +# Initialize the Mistral client +client = Mistral(api_key=api_key) + + + +# Function to generate passcodes +def generate_wagon_passcodes(theme, num_wagons): + if num_wagons <= 0 or num_wagons > 10: + return "Please provide a valid number of wagons (1-10)." + + # Prompt Mistral API to generate a theme and passcodes + prompt = f""" + This is a video game about a player trying to reach the locomotive of a train by finding a passcode for each wagon. + You are tasked with generating unique passcodes for the wagons based on the theme '{theme}', to make the game more engaging, fun, and with a sense of progression. + Each password should be unique enough to not be related to each other but still be connected to the theme. + Generate {num_wagons} unique and creative passcodes for the wagons. Each passcode must: + 1. Be related to the theme. + 2. Be unique, interesting, and creative. + 3. In one word, letters only (no spaces or special characters). + No explanation needed, just the theme and passcodes in a JSON object format. + Example: + {{ + "theme": "Pirates", + "passcodes": ["Treasure", "Rum", "Skull", "Compass", "Anchor"] + }} + Now, generate a theme and passcodes. + """ + response = client.chat.complete( + model="mistral-large-latest", + messages=[ + {"role": "user", "content": prompt} + ], + max_tokens=1000, + temperature=0.8, + ) + + try: + result = json.loads(response.choices[0].message.content.replace("```json\n", "").replace("\n```", "")) + passcodes = result["passcodes"] + except json.JSONDecodeError: + return "Failed to decode the response. Please try again." + return passcodes + +# Function to generate passengers using Mistral API +def generate_passengers_for_wagon(passcode, num_passengers): + # Generate passengers with the Mistral API + prompt = f""" + Passengers are in a wagon. The player can interact with them to learn more about their stories. + The following is a list of passengers on a train wagon. The wagon is protected by the passcode "{passcode}". + Their stories are intertwined, and each passenger has a unique role and mystery, all related to the theme and the passcode. + The player must be able to guess the passcode by talking to the passengers and uncovering their secrets. + Passengers should be diverse, with different backgrounds, professions, and motives. + Passengers' stories should be engaging, mysterious, and intriguing, adding depth to the game, while also providing clues to the passcode. + Passengers' stories has to be / can be connected to each other. + Passengers are aware of each other's presence in the wagon. + The passcode shouldn't be too obvious but should be guessable based on the passengers' stories. + The passcode shouldn't be mentioned explicitly in the passengers' descriptions. + Don't use double quotes (") in the JSON strings. + Each passenger must have the following attributes: + - "name": A unique name (first and last) with a possible title. + - "age": A realistic age between 18 and 70 except for special cases. + - "profession": A profession that fits into a fictional, story-driven world. + - "personality": A set of three adjectives that describe their character. + - "role": A short description of their role in the story. + - "mystery_intrigue": A unique secret, motive, or mystery about the character. + - "characer_model": A character model identifier + The character models are : + - character-female-a: A dark-skinned woman with a high bun hairstyle, wearing a purple and orange outfit. She is holding two blue weapons or tools, possibly a warrior or fighter. + - character-female-b: A young girl with orange hair tied into two pigtails, wearing a yellow and purple sporty outfit. She looks energetic, possibly an athlete or fitness enthusiast. + - character-female-c: An elderly woman with gray hair in a bun, wearing a blue and red dress. She has a warm and wise appearance, resembling a grandmotherly figure. + - character-female-d: A woman with blonde hair styled in a tight bun, wearing a gray business suit. She appears professional, possibly a corporate worker or manager. + - character-female-e: A woman with dark hair in a ponytail, dressed in a white lab coat with blue gloves. She likely represents a doctor or scientist. + - character-female-f: A red-haired woman with long, wavy hair, wearing a black and yellow vest with purple pants. She looks adventurous, possibly an engineer, explorer, or worker. + - character-male-a: Dark-skinned man with glasses and a beaded hairstyle, wearing a green shirt with orange and white stripes, along with yellow sneakers (casual or scholarly figure). + - character-male-b: Bald man with a large red beard, wearing a red shirt and blue pants (possibly a strong worker, blacksmith, or adventurer). + - character-male-c: Man with a mustache, wearing a blue police uniform with a cap and badge (police officer or security personnel). + - character-male-d: Blonde-haired man in a black suit with a red tie (businessman, politician, or corporate executive). + - character-male-e: Brown-haired man with glasses, wearing a white lab coat and a yellow tool belt (scientist, mechanic, or engineer). + - character-male-f: Dark-haired young man with a mustache, wearing a green vest and brown pants (possibly an explorer, traveler, or adventurer). + Generate {num_passengers} passengers in JSON array format. Example: + + [ + {{ + "name": "Victor Sterling", + "age": 55, + "profession": "Mining Magnate", + "personality": "Ambitious, cunning, and charismatic", + "role": "Owns a vast mining empire, recently discovered a new vein of precious metal.", + "mystery_intrigue": "Secretly trades in unregistered precious metals, hiding a fortune in a secure vault. In love with Eleanor Brooks", + "characer_model": "character-male-f" + }}, + {{ + "name": "Eleanor Brooks", + "age": 32, + "profession": "Investigative Journalist", + "personality": "Tenacious, curious, and ethical", + "role": "Investigates corruption in the mining industry, follows a lead on a hidden stash of radiant metal bars.", + "mystery_intrigue": "Uncovers a network of illegal precious metal trades, putting her life in danger. Hates Victor Sterling because of his unethical practices.", + "characer_model": "character-female-f" + }} + ] + + Now generate the JSON array: + """ + response = client.chat.complete( + model="mistral-large-latest", + messages=[ + {"role": "user", "content": prompt} + ], + max_tokens=1000, + temperature=0.8, + ) + + try: + passengers = json.loads(response.choices[0].message.content.replace("```json\n", "").replace("\n```", "").replace(passcode, "")) + except json.JSONDecodeError: + return "Failed to decode the response. Please try again." + return passengers + +# Gradio interface for generating both passcodes and passengers +def generate_train_json(theme, num_wagons, min_passengers, max_passengers): + try: + num_wagons = int(num_wagons) + min_passengers = int(min_passengers) + max_passengers = int(max_passengers) + except ValueError: + return "Number of wagons and passenger limits must be integers." + + if min_passengers > max_passengers: + return "Minimum passengers cannot be greater than maximum passengers." + + # Generate passcodes + passcodes = generate_wagon_passcodes(theme, num_wagons) + if isinstance(passcodes, str): # If there's an error, return it + return passcodes + + # Generate passengers for each wagon + wagons = [] + wagons.append({ + "id": 0, + "theme": "Tutorial (Start)", + "passcode": "start", + "passengers": [] + }) + for i, passcode in enumerate(passcodes): + num_passengers = random.randint(min_passengers, max_passengers) + passengers = generate_passengers_for_wagon(passcode, num_passengers) + wagons.append({"id": i + 1, "theme": theme, "passcode": passcode, "passengers": passengers}) + + return json.dumps(wagons, indent=4) + +def generate_train(theme, num_wagons): + wagons_json = generate_train_json(theme, num_wagons, 2, 10) + wagons = json.loads(wagons_json) + all_names, all_player_details, all_wagons = convert_and_return_jsons(wagons) + return all_names, all_player_details, all_wagons From 2d60abb2738e07f17395684deaa349b625867d46 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Thu, 30 Jan 2025 14:01:14 +0100 Subject: [PATCH 03/11] 'serviceify' generate_train.py using class that extends from LoggerMixin to add logging capability --- app/core/logging.py | 2 +- app/services/generate_train/generate_train.py | 374 ++++++++++-------- 2 files changed, 214 insertions(+), 162 deletions(-) diff --git a/app/core/logging.py b/app/core/logging.py index c58a44a..bedec86 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -31,7 +31,7 @@ def setup_logging() -> None: os.makedirs("logs", exist_ok=True) logger = logging.getLogger("game_jam") - logger.setLevel(logging.INFO) + logger.setLevel(logging.DEBUG) # Console handler with JSON formatting console_handler = logging.StreamHandler() diff --git a/app/services/generate_train/generate_train.py b/app/services/generate_train/generate_train.py index 6fb34b1..962fdfd 100644 --- a/app/services/generate_train/generate_train.py +++ b/app/services/generate_train/generate_train.py @@ -2,168 +2,220 @@ import os import json import random - +from fastapi import HTTPException +from typing import Tuple, Dict, Any +from app.core.logging import LoggerMixin from app.services.generate_train.convert import convert_and_return_jsons -# Get the Mistral API key from the .env file -api_key = os.getenv("MISTRAL_API_KEY") - -if not api_key: - raise ValueError("MISTRAL_API_KEY is not set in the .env file.") - -# Initialize the Mistral client -client = Mistral(api_key=api_key) - - - -# Function to generate passcodes -def generate_wagon_passcodes(theme, num_wagons): - if num_wagons <= 0 or num_wagons > 10: - return "Please provide a valid number of wagons (1-10)." - - # Prompt Mistral API to generate a theme and passcodes - prompt = f""" - This is a video game about a player trying to reach the locomotive of a train by finding a passcode for each wagon. - You are tasked with generating unique passcodes for the wagons based on the theme '{theme}', to make the game more engaging, fun, and with a sense of progression. - Each password should be unique enough to not be related to each other but still be connected to the theme. - Generate {num_wagons} unique and creative passcodes for the wagons. Each passcode must: - 1. Be related to the theme. - 2. Be unique, interesting, and creative. - 3. In one word, letters only (no spaces or special characters). - No explanation needed, just the theme and passcodes in a JSON object format. - Example: - {{ - "theme": "Pirates", - "passcodes": ["Treasure", "Rum", "Skull", "Compass", "Anchor"] - }} - Now, generate a theme and passcodes. - """ - response = client.chat.complete( - model="mistral-large-latest", - messages=[ - {"role": "user", "content": prompt} - ], - max_tokens=1000, - temperature=0.8, - ) - - try: - result = json.loads(response.choices[0].message.content.replace("```json\n", "").replace("\n```", "")) - passcodes = result["passcodes"] - except json.JSONDecodeError: - return "Failed to decode the response. Please try again." - return passcodes - -# Function to generate passengers using Mistral API -def generate_passengers_for_wagon(passcode, num_passengers): - # Generate passengers with the Mistral API - prompt = f""" - Passengers are in a wagon. The player can interact with them to learn more about their stories. - The following is a list of passengers on a train wagon. The wagon is protected by the passcode "{passcode}". - Their stories are intertwined, and each passenger has a unique role and mystery, all related to the theme and the passcode. - The player must be able to guess the passcode by talking to the passengers and uncovering their secrets. - Passengers should be diverse, with different backgrounds, professions, and motives. - Passengers' stories should be engaging, mysterious, and intriguing, adding depth to the game, while also providing clues to the passcode. - Passengers' stories has to be / can be connected to each other. - Passengers are aware of each other's presence in the wagon. - The passcode shouldn't be too obvious but should be guessable based on the passengers' stories. - The passcode shouldn't be mentioned explicitly in the passengers' descriptions. - Don't use double quotes (") in the JSON strings. - Each passenger must have the following attributes: - - "name": A unique name (first and last) with a possible title. - - "age": A realistic age between 18 and 70 except for special cases. - - "profession": A profession that fits into a fictional, story-driven world. - - "personality": A set of three adjectives that describe their character. - - "role": A short description of their role in the story. - - "mystery_intrigue": A unique secret, motive, or mystery about the character. - - "characer_model": A character model identifier - The character models are : - - character-female-a: A dark-skinned woman with a high bun hairstyle, wearing a purple and orange outfit. She is holding two blue weapons or tools, possibly a warrior or fighter. - - character-female-b: A young girl with orange hair tied into two pigtails, wearing a yellow and purple sporty outfit. She looks energetic, possibly an athlete or fitness enthusiast. - - character-female-c: An elderly woman with gray hair in a bun, wearing a blue and red dress. She has a warm and wise appearance, resembling a grandmotherly figure. - - character-female-d: A woman with blonde hair styled in a tight bun, wearing a gray business suit. She appears professional, possibly a corporate worker or manager. - - character-female-e: A woman with dark hair in a ponytail, dressed in a white lab coat with blue gloves. She likely represents a doctor or scientist. - - character-female-f: A red-haired woman with long, wavy hair, wearing a black and yellow vest with purple pants. She looks adventurous, possibly an engineer, explorer, or worker. - - character-male-a: Dark-skinned man with glasses and a beaded hairstyle, wearing a green shirt with orange and white stripes, along with yellow sneakers (casual or scholarly figure). - - character-male-b: Bald man with a large red beard, wearing a red shirt and blue pants (possibly a strong worker, blacksmith, or adventurer). - - character-male-c: Man with a mustache, wearing a blue police uniform with a cap and badge (police officer or security personnel). - - character-male-d: Blonde-haired man in a black suit with a red tie (businessman, politician, or corporate executive). - - character-male-e: Brown-haired man with glasses, wearing a white lab coat and a yellow tool belt (scientist, mechanic, or engineer). - - character-male-f: Dark-haired young man with a mustache, wearing a green vest and brown pants (possibly an explorer, traveler, or adventurer). - Generate {num_passengers} passengers in JSON array format. Example: - - [ - {{ - "name": "Victor Sterling", - "age": 55, - "profession": "Mining Magnate", - "personality": "Ambitious, cunning, and charismatic", - "role": "Owns a vast mining empire, recently discovered a new vein of precious metal.", - "mystery_intrigue": "Secretly trades in unregistered precious metals, hiding a fortune in a secure vault. In love with Eleanor Brooks", - "characer_model": "character-male-f" - }}, + +class GenerateTrainService(LoggerMixin): + def __init__(self): + self.logger.info("Initializing GenerateTrainService") + + # Get the Mistral API key from the .env file + self.api_key = os.getenv("MISTRAL_API_KEY") + + if not self.api_key: + self.logger.error("MISTRAL_API_KEY is not set in the .env file") + raise ValueError("MISTRAL_API_KEY is not set in the .env file") + + # Initialize the Mistral client + self.client = Mistral(api_key=self.api_key) + self.logger.info("Mistral client initialized successfully") + + def generate_wagon_passcodes(self, theme: str, num_wagons: int) -> list[str]: + """Generate passcodes for wagons using Mistral AI""" + self.logger.info(f"Generating passcodes for theme: {theme}, num_wagons: {num_wagons}") + + if num_wagons <= 0 or num_wagons > 10: + self.logger.error(f"Invalid number of wagons requested: {num_wagons}") + return "Please provide a valid number of wagons (1-10)." + + prompt = f""" + This is a video game about a player trying to reach the locomotive of a train by finding a passcode for each wagon. + You are tasked with generating unique passcodes for the wagons based on the theme '{theme}', to make the game more engaging, fun, and with a sense of progression. + Each password should be unique enough to not be related to each other but still be connected to the theme. + Generate {num_wagons} unique and creative passcodes for the wagons. Each passcode must: + 1. Be related to the theme. + 2. Be unique, interesting, and creative. + 3. In one word, letters only (no spaces or special characters). + No explanation needed, just the theme and passcodes in a JSON object format. + Example: {{ - "name": "Eleanor Brooks", - "age": 32, - "profession": "Investigative Journalist", - "personality": "Tenacious, curious, and ethical", - "role": "Investigates corruption in the mining industry, follows a lead on a hidden stash of radiant metal bars.", - "mystery_intrigue": "Uncovers a network of illegal precious metal trades, putting her life in danger. Hates Victor Sterling because of his unethical practices.", - "characer_model": "character-female-f" + "theme": "Pirates", + "passcodes": ["Treasure", "Rum", "Skull", "Compass", "Anchor"] }} - ] - - Now generate the JSON array: - """ - response = client.chat.complete( - model="mistral-large-latest", - messages=[ - {"role": "user", "content": prompt} - ], - max_tokens=1000, - temperature=0.8, - ) - - try: - passengers = json.loads(response.choices[0].message.content.replace("```json\n", "").replace("\n```", "").replace(passcode, "")) - except json.JSONDecodeError: - return "Failed to decode the response. Please try again." - return passengers - -# Gradio interface for generating both passcodes and passengers -def generate_train_json(theme, num_wagons, min_passengers, max_passengers): - try: - num_wagons = int(num_wagons) - min_passengers = int(min_passengers) - max_passengers = int(max_passengers) - except ValueError: - return "Number of wagons and passenger limits must be integers." - - if min_passengers > max_passengers: - return "Minimum passengers cannot be greater than maximum passengers." - - # Generate passcodes - passcodes = generate_wagon_passcodes(theme, num_wagons) - if isinstance(passcodes, str): # If there's an error, return it - return passcodes - - # Generate passengers for each wagon - wagons = [] - wagons.append({ - "id": 0, - "theme": "Tutorial (Start)", - "passcode": "start", - "passengers": [] - }) - for i, passcode in enumerate(passcodes): - num_passengers = random.randint(min_passengers, max_passengers) - passengers = generate_passengers_for_wagon(passcode, num_passengers) - wagons.append({"id": i + 1, "theme": theme, "passcode": passcode, "passengers": passengers}) - - return json.dumps(wagons, indent=4) - -def generate_train(theme, num_wagons): - wagons_json = generate_train_json(theme, num_wagons, 2, 10) - wagons = json.loads(wagons_json) - all_names, all_player_details, all_wagons = convert_and_return_jsons(wagons) - return all_names, all_player_details, all_wagons + Now, generate a theme and passcodes. + """ + + try: + response = self.client.chat.complete( + model="mistral-large-latest", + messages=[{"role": "user", "content": prompt}], + max_tokens=1000, + temperature=0.8, + ) + + result = json.loads(response.choices[0].message.content.replace("```json\n", "").replace("\n```", "")) + passcodes = result["passcodes"] + self.logger.info(f"Successfully generated {len(passcodes)} passcodes") + return passcodes + + except json.JSONDecodeError as e: + self.logger.error(f"Failed to decode Mistral response: {e}") + return "Failed to decode the response. Please try again." + except Exception as e: + self.logger.error(f"Error generating passcodes: {e}") + return f"Error generating passcodes: {str(e)}" + + def generate_passengers_for_wagon(self, passcode: str, num_passengers: int) -> list[Dict[str, Any]]: + """Generate passengers for a wagon using Mistral AI""" + self.logger.info(f"Generating {num_passengers} passengers for wagon with passcode: {passcode}") + + prompt = f""" + Passengers are in a wagon. The player can interact with them to learn more about their stories. + The following is a list of passengers on a train wagon. The wagon is protected by the passcode "{passcode}". + Their stories are intertwined, and each passenger has a unique role and mystery, all related to the theme and the passcode. + The player must be able to guess the passcode by talking to the passengers and uncovering their secrets. + Passengers should be diverse, with different backgrounds, professions, and motives. + Passengers' stories should be engaging, mysterious, and intriguing, adding depth to the game, while also providing clues to the passcode. + Passengers' stories has to be / can be connected to each other. + Passengers are aware of each other's presence in the wagon. + The passcode shouldn't be too obvious but should be guessable based on the passengers' stories. + The passcode shouldn't be mentioned explicitly in the passengers' descriptions. + Don't use double quotes (") in the JSON strings. + Each passenger must have the following attributes: + - "name": A unique name (first and last) with a possible title. + - "age": A realistic age between 18 and 70 except for special cases. + - "profession": A profession that fits into a fictional, story-driven world. + - "personality": A set of three adjectives that describe their character. + - "role": A short description of their role in the story. + - "mystery_intrigue": A unique secret, motive, or mystery about the character. + - "characer_model": A character model identifier + The character models are : + - character-female-a: A dark-skinned woman with a high bun hairstyle, wearing a purple and orange outfit. She is holding two blue weapons or tools, possibly a warrior or fighter. + - character-female-b: A young girl with orange hair tied into two pigtails, wearing a yellow and purple sporty outfit. She looks energetic, possibly an athlete or fitness enthusiast. + - character-female-c: An elderly woman with gray hair in a bun, wearing a blue and red dress. She has a warm and wise appearance, resembling a grandmotherly figure. + - character-female-d: A woman with blonde hair styled in a tight bun, wearing a gray business suit. She appears professional, possibly a corporate worker or manager. + - character-female-e: A woman with dark hair in a ponytail, dressed in a white lab coat with blue gloves. She likely represents a doctor or scientist. + - character-female-f: A red-haired woman with long, wavy hair, wearing a black and yellow vest with purple pants. She looks adventurous, possibly an engineer, explorer, or worker. + - character-male-a: Dark-skinned man with glasses and a beaded hairstyle, wearing a green shirt with orange and white stripes, along with yellow sneakers (casual or scholarly figure). + - character-male-b: Bald man with a large red beard, wearing a red shirt and blue pants (possibly a strong worker, blacksmith, or adventurer). + - character-male-c: Man with a mustache, wearing a blue police uniform with a cap and badge (police officer or security personnel). + - character-male-d: Blonde-haired man in a black suit with a red tie (businessman, politician, or corporate executive). + - character-male-e: Brown-haired man with glasses, wearing a white lab coat and a yellow tool belt (scientist, mechanic, or engineer). + - character-male-f: Dark-haired young man with a mustache, wearing a green vest and brown pants (possibly an explorer, traveler, or adventurer). + Generate {num_passengers} passengers in JSON array format. Example: + + [ + {{ + "name": "Victor Sterling", + "age": 55, + "profession": "Mining Magnate", + "personality": "Ambitious, cunning, and charismatic", + "role": "Owns a vast mining empire, recently discovered a new vein of precious metal.", + "mystery_intrigue": "Secretly trades in unregistered precious metals, hiding a fortune in a secure vault. In love with Eleanor Brooks", + "characer_model": "character-male-f" + }}, + {{ + "name": "Eleanor Brooks", + "age": 32, + "profession": "Investigative Journalist", + "personality": "Tenacious, curious, and ethical", + "role": "Investigates corruption in the mining industry, follows a lead on a hidden stash of radiant metal bars.", + "mystery_intrigue": "Uncovers a network of illegal precious metal trades, putting her life in danger. Hates Victor Sterling because of his unethical practices.", + "characer_model": "character-female-f" + }} + ] + + Now generate the JSON array: + """ + + try: + response = self.client.chat.complete( + model="mistral-large-latest", + messages=[{"role": "user", "content": prompt}], + max_tokens=1000, + temperature=0.8, + ) + + passengers = json.loads( + response.choices[0].message.content + .replace("```json\n", "") + .replace("\n```", "") + .replace(passcode, "") + ) + self.logger.info(f"Successfully generated {len(passengers)} passengers") + return passengers + + except json.JSONDecodeError as e: + self.logger.error(f"Failed to decode passenger generation response: {e}") + return "Failed to decode the response. Please try again." + except Exception as e: + self.logger.error(f"Error generating passengers: {e}") + return f"Error generating passengers: {str(e)}" + + def generate_train_json(self, theme: str, num_wagons: int, min_passengers: int = 2, max_passengers: int = 10) -> str: + """Generate complete train JSON including wagons and passengers""" + self.logger.info(f"Generating train JSON for theme: {theme}, num_wagons: {num_wagons}") + + try: + if min_passengers > max_passengers: + self.logger.error("Minimum passengers cannot be greater than maximum passengers") + return "Minimum passengers cannot be greater than maximum passengers." + + # Generate passcodes + passcodes = self.generate_wagon_passcodes(theme, num_wagons) + if isinstance(passcodes, str): # If there's an error, return it + return passcodes + + # Generate wagons with passengers + wagons = [] + wagons.append({ + "id": 0, + "theme": "Tutorial (Start)", + "passcode": "start", + "passengers": [] + }) + + for i, passcode in enumerate(passcodes): + num_passengers = random.randint(min_passengers, max_passengers) + passengers = self.generate_passengers_for_wagon(passcode, num_passengers) + wagons.append({ + "id": i + 1, + "theme": theme, + "passcode": passcode, + "passengers": passengers + }) + + self.logger.info(f"Successfully generated train with {len(wagons)} wagons") + return json.dumps(wagons, indent=4) + + except Exception as e: + self.logger.error(f"Error in generate_train_json: {e}") + return f"Error generating train: {str(e)}" + + def generate_train(self, theme: str, num_wagons: int) -> Tuple[Dict, Dict, Dict]: + """Main method to generate complete train data""" + self.logger.info(f"Starting train generation for theme: {theme}, num_wagons: {num_wagons}") + + try: + wagons_json = self.generate_train_json(theme, num_wagons, 2, 10) + wagons = json.loads(wagons_json) + + all_names, all_player_details, all_wagons = convert_and_return_jsons(wagons) + + self.logger.info("Successfully completed train generation") + return all_names, all_player_details, all_wagons + + except Exception as e: + self.logger.error(f"Error in generate_train: {e}") + raise HTTPException(status_code=500, detail=f"Failed to generate train: {str(e)}") + + +# Create a singleton instance +generate_train_service = GenerateTrainService() + +# Function to be used by the route +def generate_train(theme: str, num_wagons: int) -> Tuple[Dict, Dict, Dict]: + return generate_train_service.generate_train(theme, num_wagons) From f3cfe80749eda71c32e78661635e3f268631a543 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Thu, 30 Jan 2025 16:47:43 +0100 Subject: [PATCH 04/11] completion of generate train by dynamically creating session folder in data that contains themed train data same format as default --- app/core/logging.py | 102 +++++---- app/main.py | 17 +- app/models/session.py | 1 + app/routes/chat.py | 11 +- app/routes/generate.py | 146 ++++++++++--- app/routes/players.py | 197 +++++++++++------- app/routes/wagons.py | 25 ++- app/services/chat_service.py | 124 +++++------ app/services/generate_train/generate_train.py | 155 +++++++++----- app/services/session_service.py | 158 ++++---------- app/utils/file_management.py | 86 ++++++++ .../names.json | 69 ++++++ .../player_details.json | 109 ++++++++++ .../wagons.json | 124 +++++++++++ data/{ => default}/names.json | 0 data/{ => default}/player_details.json | 0 data/{ => default}/wagons.json | 0 17 files changed, 918 insertions(+), 406 deletions(-) create mode 100644 app/utils/file_management.py create mode 100644 data/b67636b9-08ad-449d-a06a-3d74662e006e/names.json create mode 100644 data/b67636b9-08ad-449d-a06a-3d74662e006e/player_details.json create mode 100644 data/b67636b9-08ad-449d-a06a-3d74662e006e/wagons.json rename data/{ => default}/names.json (100%) rename data/{ => default}/player_details.json (100%) rename data/{ => default}/wagons.json (100%) diff --git a/app/core/logging.py b/app/core/logging.py index bedec86..dc93705 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -2,64 +2,74 @@ import json from datetime import datetime import os +from pathlib import Path +import sys +import traceback +from typing import Dict, Any -class JSONFormatter(logging.Formatter): - """Custom JSON formatter for structured logging""" +class CustomFormatter(logging.Formatter): + """Custom formatter that includes extra fields in the message""" def format(self, record: logging.LogRecord) -> str: - log_data = { - "timestamp": datetime.utcnow().isoformat(), - "level": record.levelname, - "message": record.getMessage(), - "module": record.module, - "function": record.funcName, - "line": record.lineno - } - - # Add extra fields if they exist - if hasattr(record, "extra_data"): - log_data.update(record.extra_data) - - # Add exception info if present - if record.exc_info: - log_data["exception"] = self.formatException(record.exc_info) - - return json.dumps(log_data) + # Get the original message + message = super().format(record) + + # If there are extra fields, append them to the message + if hasattr(record, 'extra'): + extras = ' | '.join(f"{k}={v}" for k, v in record.extra.items()) + message = f"{message} | {extras}" + + return message def setup_logging() -> None: - """Configure logging for the application""" + """Configure logging with custom formatter""" # Create logs directory if it doesn't exist - os.makedirs("logs", exist_ok=True) + logs_dir = Path("logs") + logs_dir.mkdir(exist_ok=True) - logger = logging.getLogger("game_jam") - logger.setLevel(logging.DEBUG) - - # Console handler with JSON formatting - console_handler = logging.StreamHandler() - console_handler.setFormatter(JSONFormatter()) - logger.addHandler(console_handler) - - # File handler for persistent logs - file_handler = logging.FileHandler("logs/game_jam.log") - file_handler.setFormatter(JSONFormatter()) - logger.addHandler(file_handler) - - # Prevent propagation to root logger - logger.propagate = False + # Create formatters + console_formatter = CustomFormatter( + '%(asctime)s | %(levelname)s | %(name)s | %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + file_formatter = CustomFormatter( + '%(asctime)s | %(levelname)s | %(name)s | %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # Configure console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(console_formatter) + + # Configure file handler + current_time = datetime.now().strftime('%Y%m%d_%H%M%S') + file_handler = logging.FileHandler( + logs_dir / f'app_{current_time}.log', + encoding='utf-8' + ) + file_handler.setFormatter(file_formatter) + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + + # Remove any existing handlers + root_logger.handlers = [] + + # Add our handlers + root_logger.addHandler(console_handler) + root_logger.addHandler(file_handler) def get_logger(name: str) -> logging.Logger: - """Get a logger instance with the given name""" - return logging.getLogger(f"game_jam.{name}") + """Get a logger with the given name""" + return logging.getLogger(name) class LoggerMixin: """Mixin to add logging capabilities to a class""" @classmethod def get_logger(cls) -> logging.Logger: - """Get a logger for the class""" - if not hasattr(cls, "_logger"): - cls._logger = get_logger(cls.__name__) - return cls._logger - + return get_logger(cls.__name__) + @property def logger(self) -> logging.Logger: - """Get a logger for the instance""" - return self.get_logger() \ No newline at end of file + return get_logger(self.__class__.__name__) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 8fe2872..396edf7 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,11 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from app.routes import health, wagons, chat, players, generate -from app.core.logging import get_logger +from app.core.logging import get_logger, setup_logging from dotenv import load_dotenv from datetime import datetime import time +from pathlib import Path # Load environment variables load_dotenv() @@ -25,7 +26,7 @@ # 1. Configure CORS for Unity Web Requests # ----------------------------------------------------------------------------- # -# Unity’s documentation highlights the need for the following CORS headers: +# Unity's documentation highlights the need for the following CORS headers: # "Access-Control-Allow-Origin": "*", # "Access-Control-Allow-Methods": "GET, POST, OPTIONS", # "Access-Control-Allow-Headers": "Accept, X-Access-Token, X-Application-Name, X-Request-Sent-Time", @@ -35,7 +36,7 @@ # This is the simplest approach. If you need cookies or authentication headers, # switch `allow_origins` to an explicit domain and set `allow_credentials=True`. # -# Note: We also allow PUT, DELETE, etc., but that’s up to your application needs. +# Note: We also allow PUT, DELETE, etc., but that's up to your application needs. # app.add_middleware( CORSMiddleware, @@ -171,3 +172,13 @@ async def root(): "players_endpoint": "/api/players" } +# Ensure logs directory exists and setup logging at startup +@app.on_event("startup") +async def startup_event(): + # Create logs directory if it doesn't exist + logs_dir = Path("logs") + logs_dir.mkdir(exist_ok=True) + + # Initialize logging + setup_logging() + diff --git a/app/models/session.py b/app/models/session.py index 4742c0e..400e5ce 100644 --- a/app/models/session.py +++ b/app/models/session.py @@ -34,6 +34,7 @@ class UserSession(BaseModel): guessing_progress: GuessingProgress = Field(default_factory=GuessingProgress) created_at: datetime = Field(default_factory=datetime.utcnow) last_active: datetime = Field(default_factory=datetime.utcnow) + default_game: bool = Field(default=True) class Config: json_schema_extra = { diff --git a/app/routes/chat.py b/app/routes/chat.py index 2e32485..3e45114 100644 --- a/app/routes/chat.py +++ b/app/routes/chat.py @@ -35,10 +35,6 @@ class GuessResponse(BaseModel): score: float -def get_chat_service(): - return ChatService() - - def get_guess_service(): return GuessingService() @@ -59,6 +55,7 @@ class ChatMessage(BaseModel): def get_session(session_id: str) -> UserSession: """Dependency to get and validate session""" session = SessionService.get_session(session_id) + logger.info(f"Session found: {session}") if not session: raise HTTPException(status_code=404, detail="Session not found") return session @@ -137,13 +134,17 @@ async def chat_with_character( uid: str, chat_message: ChatMessage, session: UserSession = Depends(get_session), - chat_service: ChatService = Depends(get_chat_service), tts_service: TTSService = Depends(get_tts_service), ) -> dict: """ Send a message to a character and get their response. The input is a JSON containing the prompt and related data. """ + + # Get the chat service, that loads the character details + chat_service = ChatService(session) + + # add first checks that the user exists try: wagon_id = int(uid.split("-")[1]) if wagon_id != session.current_wagon.wagon_id: diff --git a/app/routes/generate.py b/app/routes/generate.py index 0d849b2..eea90d1 100644 --- a/app/routes/generate.py +++ b/app/routes/generate.py @@ -1,50 +1,134 @@ from fastapi import APIRouter, HTTPException, Depends -from app.services.session_service import SessionService -from app.models.session import UserSession -from app.services.generate_train.generate_train import generate_train +from app.services.generate_train.generate_train import GenerateTrainService +from app.services.generate_train.generate_train import GenerateTrainService from app.models.train import GenerateTrainResponse, Names, PlayerDetailsResponse, WagonsResponse +from app.utils.file_management import FileManager +from app.core.logging import get_logger +from app.services.session_service import SessionService + router = APIRouter( prefix="/api/generate", - tags=["chat"] + tags=["train-generation"] ) -@router.get("/train/{session_id}/{number_of_wagons}/{theme}", response_model=GenerateTrainResponse) -async def get_generated_train(session_id: str, number_of_wagons: str, theme: str): - # session = SessionService.get_session(session_id) - # if not session: - # raise HTTPException(status_code=404, detail="Session not found") +logger = get_logger("generate") + +@router.get( + "/train/{session_id}/{number_of_wagons}/{theme}", + response_model=GenerateTrainResponse, + summary="Generate a new train for a session", + description=""" + Generates a new train with the specified number of wagons and theme for a given session. + Each wagon contains: + - A unique passcode related to the theme + - Multiple passengers with generated names and profiles + - Theme-specific details and characteristics + """, + responses={ + 200: { + "description": "Successfully generated train data", + "content": { + "application/json": { + "example": { + "names": { + "names": { + "wagon-1": { + "player-1": {"first_name": "John", "last_name": "Doe"} + } + } + }, + "player_details": { + "player_details": { + "wagon-1": { + "player-1": {"profile": {"age": 30, "occupation": "Engineer"}} + } + } + }, + "wagons": { + "wagons": { + "wagon-1": {"theme": "Space", "passcode": "Nebula"} + } + } + } + } + } + }, + 400: { + "description": "Invalid request parameters", + "content": { + "application/json": { + "example": {"detail": "number_of_wagons must be between 1 and 6"} + } + } + }, + 404: { + "description": "Session not found", + "content": { + "application/json": { + "example": {"detail": "Session not found"} + } + } + }, + 500: { + "description": "Internal server error", + "content": { + "application/json": { + "example": {"detail": "Failed to generate train: Internal error"} + } + } + } + } +) +async def get_generated_train( + session_id: str, + number_of_wagons: str, + theme: str +) -> GenerateTrainResponse: + """ + Generate a new train with specified parameters for a session. + + - Validates the session exists + - Creates wagons with theme-appropriate passcodes + - Generates passengers with names and profiles + - Stores the generated data for the session + - Returns the complete train data structure + """ + session = SessionService.get_session(session_id) + if not session: + raise HTTPException(status_code=404, detail="Session not found") - # Convert and validate number_of_wagons try: number_of_wagons = int(number_of_wagons) except ValueError: - raise HTTPException( - status_code=400, - detail="number_of_wagons must be an integer" - ) + raise HTTPException(status_code=400, detail="number_of_wagons must be an integer") if number_of_wagons <= 0: - raise HTTPException( - status_code=400, - detail="number_of_wagons must be greater than 0" - ) + raise HTTPException(status_code=400, detail="number_of_wagons must be greater than 0") if number_of_wagons > 6: - raise HTTPException( - status_code=400, - detail="number_of_wagons cannot exceed 6" - ) + raise HTTPException(status_code=400, detail="number_of_wagons cannot exceed 6") + + try: + + generate_train_service = GenerateTrainService() + names, player_details, wagons = generate_train_service.generate_train(theme, number_of_wagons) + FileManager.save_session_data(session_id, names, player_details, wagons) - # Generate train data using the existing function - names, player_details, wagons = generate_train(theme, number_of_wagons) + response = GenerateTrainResponse( + names=Names(names=names["names"]), + player_details=PlayerDetailsResponse(player_details=player_details["player_details"]), + wagons=WagonsResponse(wagons=wagons["wagons"]) + ) - # Create Pydantic models from the response - response = GenerateTrainResponse( - names=Names(names=names["names"]), - player_details=PlayerDetailsResponse(player_details=player_details["player_details"]), - wagons=WagonsResponse(wagons=wagons["wagons"]) - ) + logger.info(f"Setting default_game to False | session_id={session_id}") + session.default_game = False + # update the central state of the session + SessionService.update_session(session) - return response + return response + + except Exception as e: + logger.error(f"Failed to generate train for session {session_id}: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to generate train: {str(e)}") \ No newline at end of file diff --git a/app/routes/players.py b/app/routes/players.py index 693e414..bade5ff 100644 --- a/app/routes/players.py +++ b/app/routes/players.py @@ -3,8 +3,10 @@ import json from pathlib import Path from app.core.logging import get_logger +from app.services.session_service import SessionService +from app.utils.file_management import FileManager -router = APIRouter() +router = APIRouter(tags=["players"]) logger = get_logger("players") @@ -42,103 +44,154 @@ def filter_player_info(complete_info: dict, properties: List[str] = None) -> dic return filtered_info -@router.get("/api/players/{wagon_id}/{player_id}") +@router.get("/api/players/{session_id}/{wagon_id}/{player_id}") async def get_player_info( + session_id: str, wagon_id: str, player_id: str, - properties: Optional[List[str]] = Query( - None, description="Filter specific properties" - ), + properties: Optional[List[str]] = Query(None, description="Filter specific properties") ): - logger.info(f"Getting player info for wagon_id={wagon_id}, player_id={player_id}") - if properties: - logger.info(f"Requested properties: {properties}") - - # Define paths to JSON files - data_dir = Path("data") - player_details_path = data_dir / "player_details.json" - names_path = data_dir / "names.json" - - # Load data from JSON files - player_details = load_json_file(player_details_path) - names_data = load_json_file(names_path) - - # Get player details + logger.info( + f"Getting player info | session_id: {session_id} | wagon_id: {wagon_id} | player_id: {player_id} | requested_properties: {properties}" + ) + try: - player_info = player_details["player_details"][wagon_id][player_id] - logger.debug(f"Found player details for {player_id} in wagon {wagon_id}") - except KeyError: - logger.error(f"Player not found: wagon_id={wagon_id}, player_id={player_id}") - raise HTTPException( - status_code=404, detail="Player not found in player details" + session = SessionService.get_session(session_id) + if not session: + logger.error(f"Session not found: {session_id}") + raise HTTPException(status_code=404, detail="Session not found") + + logger.debug( + f"Loading session data | session_id: {session_id} | default_game: {session.default_game}" ) - - # Get player name information - try: - name_info = names_data["names"][wagon_id][player_id] - logger.debug(f"Found name information for {player_id} in wagon {wagon_id}") - except KeyError: - logger.warning( - f"Name information not found for player {player_id} in wagon {wagon_id}" + + # Load data based on default_game flag + names, player_details, _ = FileManager.load_session_data(session_id, session.default_game) + + try: + # First check if player_details is contained in the loaded data + if "player_details" not in player_details: + logger.error("Missing 'player_details' key in loaded data") + raise HTTPException(status_code=404, detail="Player details not found") + + player_info = player_details["player_details"][wagon_id][player_id] + logger.debug( + f"Found player info | wagon: {wagon_id} | player: {player_id} | profile_exists: {'profile' in player_info}" + ) + + # first check if names is contained in the loaded data + if "names" not in names: + logger.error("Missing 'names' key in loaded data") + raise HTTPException(status_code=404, detail="Names not found") + + print("I was also here", names["names"][wagon_id][player_id]) + name_info = names["names"][wagon_id][player_id] + logger.debug( + f"Found name info | wagon: wagon_id | player: player_id" + ) + + print("I was here", player_info, name_info) + + # Combine information + complete_player_info = { + "id": player_id, + "name_info": name_info, + "profile": player_info.get("profile", {}) + } + + # Filter properties if specified + if properties: + logger.info( + f"Filtering player info | requested_properties: {properties} | available_properties: {list(complete_player_info.keys())}" + ) + return filter_player_info(complete_player_info, properties) + + logger.info("Successfully retrieved complete player info") + return complete_player_info + + except KeyError as e: + logger.error( + f"Failed to find player data | error: {str(e)} | wagon_id: {wagon_id} | player_id: {player_id}" + ) + raise HTTPException(status_code=404, detail="Player not found") + + except FileNotFoundError as e: + logger.error( + f"Failed to load session data | error: {str(e)} | session_id: {session_id}" ) - name_info = {} - - # Combine all information - complete_player_info = { - "id": player_id, - "name_info": name_info, - "profile": player_info.get("profile", {}), - } - - # Filter properties if specified - result = filter_player_info(complete_player_info, properties) - logger.info( - f"Successfully retrieved player info for {player_id} in wagon {wagon_id}" - ) - return result + raise HTTPException(status_code=404, detail="Session not found") -@router.get("/api/players/{wagon_id}") +@router.get("/api/players/{session_id}/{wagon_id}") async def get_wagon_players( + session_id: str, wagon_id: str, properties: Optional[List[str]] = Query( None, description="Filter specific properties (name_info, profile, traits, inventory, dialogue)", ), ): - logger.info(f"Getting all players for wagon_id={wagon_id}") + session = SessionService.get_session(session_id) + # check if session is found + if not session: + logger.error(f"Session not found: {session_id}") + raise HTTPException(status_code=404, detail="Session not found") + + logger.info(f"Getting all players for wagon_id={wagon_id} | session_id={session_id}") if properties: logger.info(f"Requested properties: {properties}") - # Define paths to JSON files - data_dir = Path("data") - player_details_path = data_dir / "player_details.json" - names_path = data_dir / "names.json" - - # Load data from JSON files - player_details = load_json_file(player_details_path) - names_data = load_json_file(names_path) - + logger.debug( + f"Loading session data | session_id={session_id} | default_game={session.default_game}" + ) + try: - wagon_players = player_details["player_details"][wagon_id] - wagon_names = names_data["names"][wagon_id] - logger.debug(f"Found {len(wagon_players)} players in wagon {wagon_id}") - + # Load data based on default_game flag + names, player_details, _ = FileManager.load_session_data(session_id, session.default_game) + + # First check if player_details is contained in the loaded data + if "player_details" not in player_details: + logger.error("Missing 'player_details' key in loaded data") + raise HTTPException(status_code=404, detail="Player details not found") + + # Check if wagon exists in player_details + if wagon_id not in player_details["player_details"]: + logger.error(f"Wagon not found | wagon_id={wagon_id}") + raise HTTPException(status_code=404, detail="Wagon not found") + + player_info = player_details["player_details"][wagon_id] + logger.debug(f"Found player info | wagon={wagon_id} | player_count={len(player_info)}") + + # Check if names data exists and is valid + if "names" not in names: + logger.error("Missing 'names' key in loaded data") + raise HTTPException(status_code=404, detail="Names not found") + + if wagon_id not in names["names"]: + logger.error(f"Wagon names not found | wagon_id={wagon_id}") + raise HTTPException(status_code=404, detail="Wagon names not found") + + name_info = names["names"][wagon_id] + logger.debug(f"Found name info | wagon={wagon_id} | name_count={len(name_info)}") + # Combine information for all players in the wagon players_info = [] - - for player_id in wagon_players: - logger.debug(f"Processing player {player_id} in wagon {wagon_id}") + for player_id in player_info: + logger.debug(f"Processing player | wagon={wagon_id} | player={player_id}") complete_info = { "id": player_id, - "name_info": wagon_names.get(player_id, {}), - "profile": wagon_players[player_id].get("profile", {}), + "name_info": name_info.get(player_id, {}), + "profile": player_info[player_id].get("profile", {}) } filtered_info = filter_player_info(complete_info, properties) players_info.append(filtered_info) - logger.info(f"Successfully retrieved all players for wagon {wagon_id}") + logger.info(f"Successfully retrieved all players | wagon={wagon_id} | player_count={len(players_info)}") return {"players": players_info} - except KeyError: - logger.error(f"Wagon not found: wagon_id={wagon_id}") - raise HTTPException(status_code=404, detail="Wagon not found") + + except FileNotFoundError as e: + logger.error(f"Failed to load session data | error={str(e)} | session_id={session_id}") + raise HTTPException(status_code=404, detail="Session data not found") + except Exception as e: + logger.error(f"Unexpected error | error={str(e)} | wagon_id={wagon_id}") + raise HTTPException(status_code=500, detail="Internal server error") diff --git a/app/routes/wagons.py b/app/routes/wagons.py index 6379335..ea4aacf 100644 --- a/app/routes/wagons.py +++ b/app/routes/wagons.py @@ -1,22 +1,25 @@ from fastapi import APIRouter, HTTPException from pathlib import Path import json +from app.services.session_service import SessionService +from app.utils.file_management import FileManager router = APIRouter( prefix="/api/wagons", tags=["wagons"] ) -@router.get("/") -async def get_wagons(): - """Get all wagons data""" +@router.get("/{session_id}") +async def get_wagons(session_id: str): + session = SessionService.get_session(session_id) + if not session: + raise HTTPException(status_code=404, detail="Session not found") + try: - file_path = Path("data/wagons.json") - if not file_path.exists(): - raise HTTPException(status_code=404, detail="Wagons data not found") - - with open(file_path, "r") as f: - wagons_data = json.load(f) + # Use default_game flag from session to determine data source + wagons_data = FileManager.load_session_data(session_id, session.default_game)[2] return wagons_data - except json.JSONDecodeError: - raise HTTPException(status_code=500, detail="Error reading wagons data") \ No newline at end of file + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading wagons data: {str(e)}") \ No newline at end of file diff --git a/app/services/chat_service.py b/app/services/chat_service.py index db0d1b1..d843db1 100644 --- a/app/services/chat_service.py +++ b/app/services/chat_service.py @@ -5,6 +5,8 @@ from typing import Optional, Dict import os from mistralai import Mistral +from app.utils.file_management import FileManager +from app.models.session import UserSession # Get the Mistral API key from environment (injected by ECS) @@ -12,17 +14,18 @@ class ChatService(LoggerMixin): - def __init__(self): + def __init__(self, session: UserSession): self.logger.info("Initializing ChatService") - self.character_details: Dict = self._load_character_details() + # Load all available character in every wagon. + self.player_details: Dict = self._load_player_details(session) - if not self.character_details: + if not self.player_details: self.logger.error( "Failed to initialize character details - dictionary is empty" ) else: self.logger.info( - f"Loaded character details for wagons: {list(self.character_details.keys())}" + f"Loaded character details for wagons: {list(self.player_details.keys())}" ) # Get the Mistral API key from environment (injected by ECS) @@ -37,40 +40,26 @@ def __init__(self): self.logger.info("Initialized Mistral AI client") @classmethod - def _load_character_details(cls) -> Dict: + def _load_player_details(cls, session) -> Dict: """Load character details from JSON files""" try: - # Get the absolute path to the data directory - current_dir = Path(__file__).resolve().parent - app_dir = current_dir.parent - backend_dir = app_dir.parent - player_details_path = backend_dir / "data" / "player_details.json" - - # use cls because it's a class method + # Use FileManager to load the default session data + _, player_details, _ = FileManager.load_session_data(session.session_id, session.default_game) + + if "player_details" not in player_details: + print("Missing 'player_details' key in JSON data") + cls.get_logger().error("Missing 'player_details' key in JSON data") + return {} + + details = player_details["player_details"] + print(f"Loaded character details: {details}") cls.get_logger().info( - f"Attempting to load character details from {player_details_path}" + f"Successfully loaded character details. Available wagons: {list(details.keys())}" ) - - # check if the file exists - if not player_details_path.exists(): - cls.get_logger().error(f"File not found: {player_details_path}") - return {} - - # read the contents of the file. - with open(player_details_path, "r") as f: - data = json.load(f) - # check if the key exists - if "player_details" not in data: - cls.get_logger().error("Missing 'player_details' key in JSON data") - return {} - # load the details about the players in the wagons - details = data["player_details"] - cls.get_logger().info( - f"Successfully loaded character details. Available wagons: {list(details.keys())}" - ) - return details - except json.JSONDecodeError as e: - cls.get_logger().error(f"JSON parsing error: {str(e)}") + return details + + except FileNotFoundError as e: + cls.get_logger().error(f"Failed to load default player details: {str(e)}") return {} except Exception as e: cls.get_logger().error(f"Failed to load character details: {str(e)}") @@ -88,41 +77,29 @@ def _get_character_context(self, uid: str) -> Optional[Dict]: ) # check if the wagon key exists - if wagon_key not in self.character_details: + print(self.player_details, "player_details") + if wagon_key not in self.player_details: self.logger.error( - f"Wagon {wagon_key} not found in character details. Available wagons: {list(self.character_details.keys())}" + f"Wagon {wagon_key} not found in character details. Available wagons: {list(self.player_details.keys())}" ) return None # check if the player key exists - if player_key not in self.character_details[wagon_key]: + if player_key not in self.player_details[wagon_key]: self.logger.error( - f"Player {player_key} not found in wagon {wagon_key}. Available players: {list(self.character_details[wagon_key].keys())}" + f"Player {player_key} not found in wagon {wagon_key}. Available players: {list(self.player_details[wagon_key].keys())}" ) return None # get the details of the player that belongs to the wagon - character = self.character_details[wagon_key][player_key] + character = self.player_details[wagon_key][player_key] self.logger.debug( - "Retrieved character context", - extra={ - "uid": uid, - "wagon": wagon_key, - "player": player_key, - "profession": character["profile"]["profession"], - }, + f"Retrieved character context | uid: {uid} | wagon: {wagon_key} | player: {player_key} | profession: {character['profile']['profession']}" ) return character except (KeyError, IndexError) as e: self.logger.error( - f"Failed to get character context: {str(e)}", - extra={ - "uid": uid, - "error": str(e), - "character_details_keys": list(self.character_details.keys()) - if self.character_details - else None, - }, + f"Failed to get character context: {str(e)} | uid: {uid} | error: {str(e)} | player_details_keys: {list(self.player_details.keys()) if self.player_details else None}" ) return None @@ -176,26 +153,33 @@ def generate_response(self, uid: str, conversation: Conversation) -> Optional[st messages.append({"role": role, "content": msg.content}) # Get response from Mistral AI - chat_response = self.client.chat.complete( - model=self.model, messages=messages, temperature=0.7, max_tokens=500 - ) + try: + chat_response = self.client.chat.complete( + model=self.model, messages=messages, temperature=0.7, max_tokens=500 + ) - response = chat_response.choices[0].message.content + if not chat_response or not chat_response.choices: + raise ValueError("Empty response received from Mistral AI") - self.logger.info( - "Generated Mistral AI response", - extra={ - "uid": uid, - "response_length": len(response), - "conversation_length": len(conversation.messages), - }, - ) + response = chat_response.choices[0].message.content + + if not response or not isinstance(response, str): + raise ValueError(f"Invalid response format: {type(response)}") + + self.logger.info( + f"Generated Mistral AI response | uid: {uid} | response_length: {len(response)} | conversation_length: {len(conversation.messages)}" + ) + + return response - return response + except Exception as api_error: + self.logger.error( + f"Mistral API error | uid: {uid} | error: {str(api_error)} | messages_count: {len(messages)}" + ) + raise ValueError(f"Mistral API error: {str(api_error)}") except Exception as e: self.logger.error( - f"Failed to generate Mistral AI response: {str(e)}", - extra={"uid": uid, "error": str(e)}, + f"Failed to generate Mistral AI response | uid: {uid} | error: {str(e)} | error_type: {type(e).__name__} | character_name: {character.get('profile', {}).get('name', 'unknown')}" ) - return "I apologize, but I'm having trouble responding right now. Please try again later." + return f"I apologize, but I'm having trouble responding right now. Error: {str(e)}" diff --git a/app/services/generate_train/generate_train.py b/app/services/generate_train/generate_train.py index 962fdfd..4f231cb 100644 --- a/app/services/generate_train/generate_train.py +++ b/app/services/generate_train/generate_train.py @@ -6,6 +6,7 @@ from typing import Tuple, Dict, Any from app.core.logging import LoggerMixin from app.services.generate_train.convert import convert_and_return_jsons +from app.core.logging import get_logger class GenerateTrainService(LoggerMixin): @@ -31,6 +32,7 @@ def generate_wagon_passcodes(self, theme: str, num_wagons: int) -> list[str]: self.logger.error(f"Invalid number of wagons requested: {num_wagons}") return "Please provide a valid number of wagons (1-10)." + # Prompt Mistral API to generate a theme and passcodes prompt = f""" This is a video game about a player trying to reach the locomotive of a train by finding a passcode for each wagon. You are tasked with generating unique passcodes for the wagons based on the theme '{theme}', to make the game more engaging, fun, and with a sense of progression. @@ -47,15 +49,16 @@ def generate_wagon_passcodes(self, theme: str, num_wagons: int) -> list[str]: }} Now, generate a theme and passcodes. """ + response = self.client.chat.complete( + model="mistral-large-latest", + messages=[ + {"role": "user", "content": prompt} + ], + max_tokens=1000, + temperature=0.8, + ) try: - response = self.client.chat.complete( - model="mistral-large-latest", - messages=[{"role": "user", "content": prompt}], - max_tokens=1000, - temperature=0.8, - ) - result = json.loads(response.choices[0].message.content.replace("```json\n", "").replace("\n```", "")) passcodes = result["passcodes"] self.logger.info(f"Successfully generated {len(passcodes)} passcodes") @@ -72,6 +75,7 @@ def generate_passengers_for_wagon(self, passcode: str, num_passengers: int) -> l """Generate passengers for a wagon using Mistral AI""" self.logger.info(f"Generating {num_passengers} passengers for wagon with passcode: {passcode}") + # Generate passengers with the Mistral API prompt = f""" Passengers are in a wagon. The player can interact with them to learn more about their stories. The following is a list of passengers on a train wagon. The wagon is protected by the passcode "{passcode}". @@ -130,21 +134,18 @@ def generate_passengers_for_wagon(self, passcode: str, num_passengers: int) -> l Now generate the JSON array: """ + response = self.client.chat.complete( + model="mistral-large-latest", + messages=[ + {"role": "user", "content": prompt} + ], + max_tokens=1000, + temperature=0.8, + ) - try: - response = self.client.chat.complete( - model="mistral-large-latest", - messages=[{"role": "user", "content": prompt}], - max_tokens=1000, - temperature=0.8, - ) - passengers = json.loads( - response.choices[0].message.content - .replace("```json\n", "") - .replace("\n```", "") - .replace(passcode, "") - ) + try: + passengers = json.loads(response.choices[0].message.content.replace("```json\n", "").replace("\n```", "").replace(passcode, "")) self.logger.info(f"Successfully generated {len(passengers)} passengers") return passengers @@ -162,60 +163,116 @@ def generate_train_json(self, theme: str, num_wagons: int, min_passengers: int = try: if min_passengers > max_passengers: self.logger.error("Minimum passengers cannot be greater than maximum passengers") - return "Minimum passengers cannot be greater than maximum passengers." + raise ValueError("Minimum passengers cannot be greater than maximum passengers") # Generate passcodes passcodes = self.generate_wagon_passcodes(theme, num_wagons) - if isinstance(passcodes, str): # If there's an error, return it - return passcodes - + if isinstance(passcodes, str): # If there's an error message + self.logger.error(f"Error generating passcodes: {passcodes}") + raise ValueError(f"Failed to generate passcodes: {passcodes}") + # Generate wagons with passengers wagons = [] wagons.append({ - "id": 0, - "theme": "Tutorial (Start)", - "passcode": "start", - "passengers": [] + "id": 0, + "theme": "Tutorial (Start)", + "passcode": "start", + "passengers": [] }) - for i, passcode in enumerate(passcodes): num_passengers = random.randint(min_passengers, max_passengers) passengers = self.generate_passengers_for_wagon(passcode, num_passengers) - wagons.append({ - "id": i + 1, - "theme": theme, - "passcode": passcode, - "passengers": passengers - }) + # Check if passengers is a string (error message) + if isinstance(passengers, str): + self.logger.error(f"Error generating passengers: {passengers}") + raise ValueError(f"Failed to generate passengers: {passengers}") + wagons.append({"id": i + 1, "theme": theme, "passcode": passcode, "passengers": passengers}) self.logger.info(f"Successfully generated train with {len(wagons)} wagons") return json.dumps(wagons, indent=4) except Exception as e: self.logger.error(f"Error in generate_train_json: {e}") - return f"Error generating train: {str(e)}" + raise ValueError(f"Failed to generate train: {str(e)}") def generate_train(self, theme: str, num_wagons: int) -> Tuple[Dict, Dict, Dict]: """Main method to generate complete train data""" - self.logger.info(f"Starting train generation for theme: {theme}, num_wagons: {num_wagons}") + self.logger.info( + "Starting train generation", + extra={ + "theme": theme, + "num_wagons": num_wagons, + "service": "GenerateTrainService" + } + ) try: + # Log attempt to generate train JSON + self.logger.debug( + "Generating train JSON", + extra={ + "theme": theme, + "num_wagons": num_wagons, + "min_passengers": 2, + "max_passengers": 10 + } + ) + wagons_json = self.generate_train_json(theme, num_wagons, 2, 10) + + # Log successful JSON generation and parse attempt + self.logger.debug( + "Train JSON generated, parsing to dict", + extra={ + "json_length": len(wagons_json) + } + ) + wagons = json.loads(wagons_json) + # Log conversion attempt + self.logger.debug( + "Converting wagon data to final format", + extra={ + "num_wagons": len(wagons) + } + ) + all_names, all_player_details, all_wagons = convert_and_return_jsons(wagons) - self.logger.info("Successfully completed train generation") - return all_names, all_player_details, all_wagons + # Log successful generation with summary + self.logger.info( + "Train generation completed successfully", + extra={ + "theme": theme, + "total_wagons": len(all_wagons["wagons"]), + "total_names": len(all_names["names"]), + "total_player_details": len(all_player_details["player_details"]) + } + ) - except Exception as e: - self.logger.error(f"Error in generate_train: {e}") - raise HTTPException(status_code=500, detail=f"Failed to generate train: {str(e)}") - - -# Create a singleton instance -generate_train_service = GenerateTrainService() + return all_names, all_player_details, all_wagons -# Function to be used by the route -def generate_train(theme: str, num_wagons: int) -> Tuple[Dict, Dict, Dict]: - return generate_train_service.generate_train(theme, num_wagons) + except json.JSONDecodeError as e: + self.logger.error( + "JSON parsing error in generate_train", + extra={ + "error_type": "JSONDecodeError", + "error_msg": str(e), + "theme": theme, + "num_wagons": num_wagons + } + ) + raise HTTPException(status_code=500, detail=f"Failed to parse train JSON: {str(e)}") + + except Exception as e: + self.logger.error( + "Error in generate_train", + extra={ + "error_type": type(e).__name__, + "error_msg": str(e), + "theme": theme, + "num_wagons": num_wagons + } + ) + raise HTTPException(status_code=500, detail=f"Failed to generate train: {str(e)}") \ No newline at end of file diff --git a/app/services/session_service.py b/app/services/session_service.py index 4a104f0..feee279 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -8,8 +8,7 @@ GuessingProgress, ) from app.core.logging import LoggerMixin -import json -from pathlib import Path +import uuid # used as dependency injection for the session service @@ -19,44 +18,38 @@ class SessionService(LoggerMixin): @classmethod def create_session(cls) -> UserSession: - """Create a new user session""" - # create a new session and store it in the dictionary - session = UserSession() - cls._sessions[session.session_id] = session - cls.get_logger().info( - "Created new session", extra={"session_id": session.session_id} + """Create a new session""" + session_id = str(uuid.uuid4()) + + session = UserSession( + session_id=session_id, + created_at=datetime.utcnow(), + last_active=datetime.utcnow(), + default_game=True ) + + cls._sessions[session_id] = session + cls.get_logger().info(f"Created new session: {session_id}") return session @classmethod def get_session(cls, session_id: str) -> Optional[UserSession]: - """Get an existing session by ID""" - # get the session from the dictionary + """Get session by ID""" session = cls._sessions.get(session_id) - if session: - cls.get_logger().debug( - "Retrieved session", extra={"session_id": session_id} - ) + session.last_active = datetime.utcnow() + cls.get_logger().debug(f"Retrieved session: {session_id}") else: - cls.get_logger().warning( - "Session not found", extra={"session_id": session_id} - ) + cls.get_logger().warning(f"Session not found: {session_id}") return session @classmethod def update_session(cls, session: UserSession) -> None: """Update a session's last active timestamp""" - # update the last active timestamp session.last_active = datetime.utcnow() - # update the session in the dictionary by overriding the existing session cls._sessions[session.session_id] = session cls.get_logger().debug( - "Updated session", - extra={ - "session_id": session.session_id, - "current_wagon": session.current_wagon.wagon_id, - }, + f"Updated session | session_id: {session.session_id} | current_wagon: {session.current_wagon.wagon_id}" ) @classmethod @@ -64,13 +57,10 @@ def add_message( cls, session_id: str, uid: str, message: Message ) -> Optional[Conversation]: """Add a message to a character's conversation""" - # get the session from the dictionary session = cls.get_session(session_id) - # check if the session exists if not session: cls.get_logger().error( - "Failed to add message - session not found", - extra={"session_id": session_id, "uid": uid}, + f"Failed to add message - session not found | session_id: {session_id} | uid: {uid}" ) return None @@ -82,20 +72,14 @@ def add_message( # which might indicate out of sync in wagon. if wagon_id != session.current_wagon.wagon_id: cls.get_logger().error( - "Cannot add message - wrong wagon", - extra={ - "session_id": session_id, - "uid": uid, - "current_wagon": session.current_wagon.wagon_id, - }, + f"Cannot add message - wrong wagon | session_id: {session_id} | uid: {uid} | current_wagon: {session.current_wagon.wagon_id}" ) return None # in case we have not started a conversation with this character yet, start one if uid not in session.current_wagon.conversations: cls.get_logger().info( - "Starting new conversation", - extra={"session_id": session_id, "uid": uid, "wagon_id": wagon_id}, + f"Starting new conversation | session_id: {session_id} | uid: {uid} | wagon_id: {wagon_id}" ) session.current_wagon.conversations[uid] = Conversation(uid=uid) @@ -106,13 +90,7 @@ def add_message( cls.update_session(session) cls.get_logger().debug( - "Added message to conversation", - extra={ - "session_id": session_id, - "uid": uid, - "message_role": message.role, - "message_length": len(message.content), - }, + f"Added message to conversation | session_id: {session_id} | uid: {uid} | message_role: {message.role} | message_length: {len(message.content)}" ) return conversation @@ -120,11 +98,9 @@ def add_message( def get_conversation(cls, session_id: str, uid: str) -> Optional[Conversation]: """Get a conversation with a specific character""" session = cls.get_session(session_id) - # check if the session exists if not session: cls.get_logger().error( - "Failed to get conversation - session not found", - extra={"session_id": session_id, "uid": uid}, + f"Failed to get conversation - session not found | session_id: {session_id} | uid: {uid}" ) return None @@ -137,12 +113,7 @@ def get_conversation(cls, session_id: str, uid: str) -> Optional[Conversation]: # which might indicate out of sync in wagon. if wagon_id != session.current_wagon.wagon_id: cls.get_logger().warning( - "Cannot get conversation - wrong wagon", - extra={ - "session_id": session_id, - "uid": uid, - "current_wagon": session.current_wagon.wagon_id, - }, + f"Cannot get conversation - wrong wagon | session_id: {session_id} | uid: {uid} | current_wagon: {session.current_wagon.wagon_id}" ) return None @@ -151,30 +122,22 @@ def get_conversation(cls, session_id: str, uid: str) -> Optional[Conversation]: if conversation: cls.get_logger().debug( - "Retrieved conversation", - extra={ - "session_id": session_id, - "uid": uid, - "message_count": len(conversation.messages), - }, + f"Retrieved conversation | session_id: {session_id} | uid: {uid} | message_count: {len(conversation.messages)}" ) else: cls.get_logger().debug( - "No conversation found", extra={"session_id": session_id, "uid": uid} + f"No conversation found | session_id: {session_id} | uid: {uid}" ) return conversation @classmethod def get_guessing_progress(cls, session_id: str) -> GuessingProgress: session = cls.get_session(session_id) - if not session: cls.get_logger().error( - "Failed to get guesses - session not found", - extra={"session_id": session_id}, + f"Failed to get guesses - session not found | session_id: {session_id}" ) - return - + return None return session.guessing_progress @classmethod @@ -182,11 +145,9 @@ def update_guessing_progress( cls, session_id: str, indication: str, guess: str, thought: list[str] ) -> None: session = cls.get_session(session_id) - if not session: cls.get_logger().error( - "Failed to get the guessing progress - session not found", - extra={"session_id": session_id}, + f"Failed to get the guessing progress - session not found | session_id: {session_id}" ) return @@ -216,10 +177,7 @@ def update_guessing_progress( messages.append(Message(role="assistant", content=thought[0])) cls.update_session(session) - cls.get_logger().info( - "Added a new guess", - extra={"session_id": session_id}, - ) + cls.get_logger().info(f"Added a new guess | session_id: {session_id}") @classmethod def advance_wagon(cls, session_id: str) -> bool: @@ -227,42 +185,24 @@ def advance_wagon(cls, session_id: str) -> bool: session = cls.get_session(session_id) if not session: cls.get_logger().error( - "Failed to advance wagon - session not found", - extra={"session_id": session_id}, - ) - return False - - wagons_file = Path("data/wagons.json") - try: - with open(wagons_file, "r") as f: - wagons_data = json.load(f) - # we start counting from 0, max_wagon_id is the last wagon id - max_wagon_id = len(wagons_data["wagons"]) - 1 - except Exception as e: - cls.get_logger().error( - "Failed to read wagons data", - extra={"session_id": session_id, "error": str(e)}, + f"Failed to advance wagon - session not found | session_id: {session_id}" ) return False # advance to the next wagon next_wagon_id = session.current_wagon.wagon_id + 1 # check if we are out of bounds - if next_wagon_id > max_wagon_id: + if next_wagon_id > 2: # Assuming max_wagon_id is 2 for this example cls.get_logger().warning( - "Cannot advance - already at last wagon", - extra={ - "session_id": session_id, - "current_wagon": session.current_wagon.wagon_id, - }, + f"Cannot advance - already at last wagon | session_id: {session_id} | current_wagon: {session.current_wagon.wagon_id}" ) return False # Set up next wagon session.current_wagon = WagonProgress( wagon_id=next_wagon_id, - theme=wagons_data["wagons"][next_wagon_id]["theme"], - password=wagons_data["wagons"][next_wagon_id]["passcode"], + theme="New Theme", + password="New Passcode", ) # Clean up the previous guesses @@ -270,8 +210,7 @@ def advance_wagon(cls, session_id: str) -> bool: cls.update_session(session) cls.get_logger().info( - "Advanced to next wagon", - extra={"session_id": session_id, "new_wagon": next_wagon_id}, + f"Advanced to next wagon | session_id: {session_id} | new_wagon: {next_wagon_id}" ) return True @@ -286,35 +225,16 @@ def cleanup_old_sessions(cls, max_age_hours: int = 24) -> None: if age > max_age_hours: sessions_to_remove.append(session_id) cls.get_logger().info( - "Marking session for cleanup", - extra={"session_id": session_id, "age_hours": age}, + f"Marking session for cleanup | session_id: {session_id} | age_hours: {age}" ) for session_id in sessions_to_remove: - del cls._sessions[session_id] - cls.get_logger().info( - "Cleaned up old session", extra={"session_id": session_id} - ) + cls.terminate_session(session_id) + cls.get_logger().info(f"Cleaned up old session | session_id: {session_id}") @classmethod def terminate_session(cls, session_id: str) -> None: """Terminate a session and clean up its resources""" - session = cls.get_session(session_id) - if not session: - cls.get_logger().warning( - "Attempted to terminate non-existent session", - extra={"session_id": session_id}, - ) - return - - # Clean up session data if session_id in cls._sessions: del cls._sessions[session_id] - cls.get_logger().info( - "Session terminated", - extra={ - "session_id": session_id, - "terminated_at": datetime.utcnow().isoformat(), - "final_wagon": session.current_wagon.wagon_id, - }, - ) + cls.get_logger().info(f"Terminated session: {session_id}") diff --git a/app/utils/file_management.py b/app/utils/file_management.py new file mode 100644 index 0000000..400ad45 --- /dev/null +++ b/app/utils/file_management.py @@ -0,0 +1,86 @@ +from pathlib import Path +import json +import shutil +from typing import Dict, Any +from app.core.logging import LoggerMixin + +class FileManager(LoggerMixin): + BASE_DATA_DIR = Path("data") + DEFAULT_DIR = BASE_DATA_DIR / "default" + + @classmethod + def ensure_directories(cls) -> None: + """Ensure all required directories exist""" + cls.BASE_DATA_DIR.mkdir(exist_ok=True) + cls.DEFAULT_DIR.mkdir(exist_ok=True) + + @classmethod + def create_session_directory(cls, session_id: str) -> Path: + """Create a new directory for the session""" + session_dir = cls.BASE_DATA_DIR / session_id + session_dir.mkdir(exist_ok=True) + return session_dir + + @classmethod + def save_session_data(cls, session_id: str, names: Dict, player_details: Dict, wagons: Dict) -> None: + """Save the three main data files for a session""" + session_dir = cls.create_session_directory(session_id) + + # Save each file + files_to_save = { + "names.json": names, + "player_details.json": player_details, + "wagons.json": wagons + } + + for filename, data in files_to_save.items(): + file_path = session_dir / filename + cls.save_json(file_path, data) + cls.get_logger().info(f"Saved {filename} for session {session_id}") + + @classmethod + def get_data_directory(cls, session_id: str, default_game: bool) -> Path: + """Get the appropriate data directory based on default_game flag""" + if default_game: + return cls.DEFAULT_DIR + return cls.BASE_DATA_DIR / session_id + + @classmethod + def load_session_data(cls, session_id: str, default_game: bool = True) -> tuple[Dict, Dict, Dict]: + """Load all data files for a session""" + data_dir = cls.get_data_directory(session_id, default_game) + print("Data directory: ", data_dir) + if not data_dir.exists(): + cls.get_logger().error(f"Data directory not found: {data_dir}") + raise FileNotFoundError(f"No data found for session {session_id}") + + try: + names = cls.load_json(data_dir / "names.json") + player_details = cls.load_json(data_dir / "player_details.json") + wagons = cls.load_json(data_dir / "wagons.json") + + cls.get_logger().info( + f"Loaded session data from {'default' if default_game else 'session'} directory", + extra={ + "session_id": session_id, + "directory": str(data_dir) + } + ) + print(player_details, "jasjdfjasdfjasjdfjsa") + return names, player_details, wagons + + except FileNotFoundError as e: + cls.get_logger().error(f"Failed to load required files from {data_dir}: {str(e)}") + raise FileNotFoundError(f"Missing required data files in {data_dir}") + + @staticmethod + def save_json(file_path: Path, data: Dict[str, Any]) -> None: + """Save data to a JSON file""" + with open(file_path, 'w') as f: + json.dump(data, f, indent=2) + + @staticmethod + def load_json(file_path: Path) -> Dict: + """Load data from a JSON file""" + with open(file_path, 'r') as f: + return json.load(f) \ No newline at end of file diff --git a/data/b67636b9-08ad-449d-a06a-3d74662e006e/names.json b/data/b67636b9-08ad-449d-a06a-3d74662e006e/names.json new file mode 100644 index 0000000..5251080 --- /dev/null +++ b/data/b67636b9-08ad-449d-a06a-3d74662e006e/names.json @@ -0,0 +1,69 @@ +{ + "names": { + "wagon-0": {}, + "wagon-1": { + "player-1": { + "firstName": "Dr. Amelia", + "lastName": "Hartford", + "sex": "female", + "fullName": "Dr. Amelia Hartford" + }, + "player-2": { + "firstName": "Alexander", + "lastName": "Blackwood", + "sex": "male", + "fullName": "Alexander Blackwood" + }, + "player-3": { + "firstName": "Prof. Marcus", + "lastName": "Ravenwood", + "sex": "male", + "fullName": "Prof. Marcus Ravenwood" + } + }, + "wagon-2": { + "player-1": { + "firstName": "Dr. Olivia", + "lastName": "Hartford", + "sex": "female", + "fullName": "Dr. Olivia Hartford" + }, + "player-2": { + "firstName": "Elias", + "lastName": "Blackwood", + "sex": "male", + "fullName": "Elias Blackwood" + }, + "player-3": { + "firstName": "Seraphina", + "lastName": "Vale", + "sex": "female", + "fullName": "Seraphina Vale" + }, + "player-4": { + "firstName": "Prof. Alfred", + "lastName": "Mason", + "sex": "male", + "fullName": "Prof. Alfred Mason" + }, + "player-5": { + "firstName": "Lila", + "lastName": "Stone", + "sex": "female", + "fullName": "Lila Stone" + }, + "player-6": { + "firstName": "Det. Noah", + "lastName": "Thompson", + "sex": "male", + "fullName": "Det. Noah Thompson" + }, + "player-7": { + "firstName": "Sir Richard", + "lastName": "LeFevre", + "sex": "male", + "fullName": "Sir Richard LeFevre" + } + } + } +} \ No newline at end of file diff --git a/data/b67636b9-08ad-449d-a06a-3d74662e006e/player_details.json b/data/b67636b9-08ad-449d-a06a-3d74662e006e/player_details.json new file mode 100644 index 0000000..d72a003 --- /dev/null +++ b/data/b67636b9-08ad-449d-a06a-3d74662e006e/player_details.json @@ -0,0 +1,109 @@ +{ + "player_details": { + "wagon-0": {}, + "wagon-1": { + "player-1": { + "profile": { + "name": "Dr. Amelia Hartford", + "age": 45, + "profession": "Archaeologist", + "personality": "Intelligent, curious, and determined", + "role": "Discovered an ancient dragon relic, currently researching its origins.", + "mystery_intrigue": "Believes the relic holds a powerful secret, possibly related to the mythical creatures of old." + } + }, + "player-2": { + "profile": { + "name": "Alexander Blackwood", + "age": 28, + "profession": "Adventurer", + "personality": "Brave, adventurous, and resourceful", + "role": "Frequently travels to hidden and dangerous locations, seeking ancient treasures.", + "mystery_intrigue": "Has a mysterious dragon tattoo, which he believes is connected to his destiny and the end of the world." + } + }, + "player-3": { + "profile": { + "name": "Prof. Marcus Ravenwood", + "age": 60, + "profession": "Historian", + "personality": "Wise, scholarly, and secretive", + "role": "Expert in ancient myths and legends, particularly dragons.", + "mystery_intrigue": "Holds a key piece of information about an ancient dragon prophecy, which he keeps hidden from the world." + } + } + }, + "wagon-2": { + "player-1": { + "profile": { + "name": "Dr. Olivia Hartford", + "age": 42, + "profession": "Geologist", + "personality": "Intellectual, reserved, and meticulous", + "role": "Conducts research on rare Earth metals, particularly interested in a new, indestructible metal.", + "mystery_intrigue": "Discovered the properties of the rare metal after studying ancient artifacts, close to revealing its origins." + } + }, + "player-2": { + "profile": { + "name": "Elias Blackwood", + "age": 30, + "profession": "Blacksmith", + "personality": "Strong-willed, rugged, and loyal", + "role": "Crafts weapons and tools from unusual metals, recently received an anonymous commission to forge a special item.", + "mystery_intrigue": "Knows the secret smelting process for the indestructible metal, handed down through his family for generations." + } + }, + "player-3": { + "profile": { + "name": "Seraphina Vale", + "age": 28, + "profession": "Historian", + "personality": "Curious, imaginative, and passionate", + "role": "Studies ancient civilizations, recently found a hidden map leading to a forgotten mine.", + "mystery_intrigue": "Possesses a piece of an ancient manuscript that hints at a metal capable of resisting any force, seeks to decipher its meaning." + } + }, + "player-4": { + "profile": { + "name": "Prof. Alfred Mason", + "age": 58, + "profession": "Archaeologist", + "personality": "Analytical, patient, and scholarly", + "role": "Uncovers ancient artifacts, recently discovered a strange metal artifact from a lost civilization.", + "mystery_intrigue": "The artifact he found is made of the same indestructible metal, links to an ancient legend of eternal strength." + } + }, + "player-5": { + "profile": { + "name": "Lila Stone", + "age": 22, + "profession": "Jeweler", + "personality": "Creative, determined, and ambitious", + "role": "Designs and crafts unique jewelry, recently acquired a rare metal to create an exclusive collection.", + "mystery_intrigue": "The rare metal she acquired is the same as the one mentioned in the ancient manuscript, unaware of its true value." + } + }, + "player-6": { + "profile": { + "name": "Det. Noah Thompson", + "age": 45, + "profession": "Detective", + "personality": "Diligent, intuitive, and just", + "role": "Investigates a series of thefts related to rare metals, recently discovered a link to an underground market.", + "mystery_intrigue": "Closely tracking the movements of the indestructible metal, believes it is the key to solving his case." + } + }, + "player-7": { + "profile": { + "name": "Sir Richard LeFevre", + "age": 65, + "profession": "Antiquities Collector", + "personality": "Elegant, shrewd, and eccentric", + "role": "Collects rare and valuable antiques, recently acquired an ancient metal ingot from a mysterious source.", + "mystery_intrigue": "The ingot he possesses is made of the indestructible metal, knows its true worth and the legends surrounding it." + } + } + } + } +} \ No newline at end of file diff --git a/data/b67636b9-08ad-449d-a06a-3d74662e006e/wagons.json b/data/b67636b9-08ad-449d-a06a-3d74662e006e/wagons.json new file mode 100644 index 0000000..83d177c --- /dev/null +++ b/data/b67636b9-08ad-449d-a06a-3d74662e006e/wagons.json @@ -0,0 +1,124 @@ +{ + "wagons": [ + { + "id": 0, + "theme": "Tutorial (Start)", + "passcode": "start", + "people": [] + }, + { + "id": 1, + "theme": "minecraft", + "passcode": "Enderdragon", + "people": [ + { + "uid": "wagon-1-player-1", + "position": [ + 0.15, + 0.63 + ], + "rotation": 0.44, + "model_type": "character-female-e", + "items": [] + }, + { + "uid": "wagon-1-player-2", + "position": [ + 0.23, + 0.12 + ], + "rotation": 0.19, + "model_type": "character-male-f", + "items": [] + }, + { + "uid": "wagon-1-player-3", + "position": [ + 0.01, + 0.66 + ], + "rotation": 0.63, + "model_type": "character-male-a", + "items": [] + } + ] + }, + { + "id": 2, + "theme": "minecraft", + "passcode": "Netherite", + "people": [ + { + "uid": "wagon-2-player-1", + "position": [ + 0.94, + 0.89 + ], + "rotation": 0.97, + "model_type": "character-female-e", + "items": [] + }, + { + "uid": "wagon-2-player-2", + "position": [ + 0.21, + 0.35 + ], + "rotation": 0.43, + "model_type": "character-male-b", + "items": [] + }, + { + "uid": "wagon-2-player-3", + "position": [ + 0.25, + 0.82 + ], + "rotation": 0.26, + "model_type": "character-female-a", + "items": [] + }, + { + "uid": "wagon-2-player-4", + "position": [ + 0.39, + 0.16 + ], + "rotation": 0.71, + "model_type": "character-male-a", + "items": [] + }, + { + "uid": "wagon-2-player-5", + "position": [ + 0.39, + 0.7 + ], + "rotation": 0.57, + "model_type": "character-female-b", + "items": [] + }, + { + "uid": "wagon-2-player-6", + "position": [ + 0.62, + 0.96 + ], + "rotation": 0.95, + "model_type": "character-male-c", + "items": [] + }, + { + "uid": "wagon-2-player-7", + "position": [ + 0.18, + 0.25 + ], + "rotation": 0.25, + "model_type": "character-male-d", + "items": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/data/names.json b/data/default/names.json similarity index 100% rename from data/names.json rename to data/default/names.json diff --git a/data/player_details.json b/data/default/player_details.json similarity index 100% rename from data/player_details.json rename to data/default/player_details.json diff --git a/data/wagons.json b/data/default/wagons.json similarity index 100% rename from data/wagons.json rename to data/default/wagons.json From db8894f6fbfdf36422158da207a9bfbcf58ec6a3 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Thu, 30 Jan 2025 17:50:43 +0100 Subject: [PATCH 05/11] allow_origins back to * --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 396edf7..250ebd8 100644 --- a/app/main.py +++ b/app/main.py @@ -40,7 +40,7 @@ # app.add_middleware( CORSMiddleware, - allow_origins=["https://mistral-ai-game-jam-neuraljam.static.hf.space"], + allow_origins=["*"], allow_credentials=False, # Must be False if allow_origins=["*"] in modern browsers allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"], allow_headers=["*"], # You could limit this to ["Accept", "X-Access-Token", ...] if desired From 031c013eea7a0f2ea738707f49ef843eece67597 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Fri, 31 Jan 2025 12:53:42 +0100 Subject: [PATCH 06/11] decrease temperature of 0.8 to 0.7 and max_tokens from 1000 to 1250 in generate train service --- app/services/generate_train/generate_train.py | 4 +- .../names.json | 69 ---------- .../player_details.json | 109 --------------- .../wagons.json | 124 ------------------ .../names.json | 45 +++++++ .../player_details.json | 69 ++++++++++ .../wagons.json | 84 ++++++++++++ 7 files changed, 200 insertions(+), 304 deletions(-) delete mode 100644 data/b67636b9-08ad-449d-a06a-3d74662e006e/names.json delete mode 100644 data/b67636b9-08ad-449d-a06a-3d74662e006e/player_details.json delete mode 100644 data/b67636b9-08ad-449d-a06a-3d74662e006e/wagons.json create mode 100644 data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/names.json create mode 100644 data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/player_details.json create mode 100644 data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/wagons.json diff --git a/app/services/generate_train/generate_train.py b/app/services/generate_train/generate_train.py index 4f231cb..d98e63a 100644 --- a/app/services/generate_train/generate_train.py +++ b/app/services/generate_train/generate_train.py @@ -139,8 +139,8 @@ def generate_passengers_for_wagon(self, passcode: str, num_passengers: int) -> l messages=[ {"role": "user", "content": prompt} ], - max_tokens=1000, - temperature=0.8, + max_tokens=1250, + temperature=0.7, ) diff --git a/data/b67636b9-08ad-449d-a06a-3d74662e006e/names.json b/data/b67636b9-08ad-449d-a06a-3d74662e006e/names.json deleted file mode 100644 index 5251080..0000000 --- a/data/b67636b9-08ad-449d-a06a-3d74662e006e/names.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "names": { - "wagon-0": {}, - "wagon-1": { - "player-1": { - "firstName": "Dr. Amelia", - "lastName": "Hartford", - "sex": "female", - "fullName": "Dr. Amelia Hartford" - }, - "player-2": { - "firstName": "Alexander", - "lastName": "Blackwood", - "sex": "male", - "fullName": "Alexander Blackwood" - }, - "player-3": { - "firstName": "Prof. Marcus", - "lastName": "Ravenwood", - "sex": "male", - "fullName": "Prof. Marcus Ravenwood" - } - }, - "wagon-2": { - "player-1": { - "firstName": "Dr. Olivia", - "lastName": "Hartford", - "sex": "female", - "fullName": "Dr. Olivia Hartford" - }, - "player-2": { - "firstName": "Elias", - "lastName": "Blackwood", - "sex": "male", - "fullName": "Elias Blackwood" - }, - "player-3": { - "firstName": "Seraphina", - "lastName": "Vale", - "sex": "female", - "fullName": "Seraphina Vale" - }, - "player-4": { - "firstName": "Prof. Alfred", - "lastName": "Mason", - "sex": "male", - "fullName": "Prof. Alfred Mason" - }, - "player-5": { - "firstName": "Lila", - "lastName": "Stone", - "sex": "female", - "fullName": "Lila Stone" - }, - "player-6": { - "firstName": "Det. Noah", - "lastName": "Thompson", - "sex": "male", - "fullName": "Det. Noah Thompson" - }, - "player-7": { - "firstName": "Sir Richard", - "lastName": "LeFevre", - "sex": "male", - "fullName": "Sir Richard LeFevre" - } - } - } -} \ No newline at end of file diff --git a/data/b67636b9-08ad-449d-a06a-3d74662e006e/player_details.json b/data/b67636b9-08ad-449d-a06a-3d74662e006e/player_details.json deleted file mode 100644 index d72a003..0000000 --- a/data/b67636b9-08ad-449d-a06a-3d74662e006e/player_details.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "player_details": { - "wagon-0": {}, - "wagon-1": { - "player-1": { - "profile": { - "name": "Dr. Amelia Hartford", - "age": 45, - "profession": "Archaeologist", - "personality": "Intelligent, curious, and determined", - "role": "Discovered an ancient dragon relic, currently researching its origins.", - "mystery_intrigue": "Believes the relic holds a powerful secret, possibly related to the mythical creatures of old." - } - }, - "player-2": { - "profile": { - "name": "Alexander Blackwood", - "age": 28, - "profession": "Adventurer", - "personality": "Brave, adventurous, and resourceful", - "role": "Frequently travels to hidden and dangerous locations, seeking ancient treasures.", - "mystery_intrigue": "Has a mysterious dragon tattoo, which he believes is connected to his destiny and the end of the world." - } - }, - "player-3": { - "profile": { - "name": "Prof. Marcus Ravenwood", - "age": 60, - "profession": "Historian", - "personality": "Wise, scholarly, and secretive", - "role": "Expert in ancient myths and legends, particularly dragons.", - "mystery_intrigue": "Holds a key piece of information about an ancient dragon prophecy, which he keeps hidden from the world." - } - } - }, - "wagon-2": { - "player-1": { - "profile": { - "name": "Dr. Olivia Hartford", - "age": 42, - "profession": "Geologist", - "personality": "Intellectual, reserved, and meticulous", - "role": "Conducts research on rare Earth metals, particularly interested in a new, indestructible metal.", - "mystery_intrigue": "Discovered the properties of the rare metal after studying ancient artifacts, close to revealing its origins." - } - }, - "player-2": { - "profile": { - "name": "Elias Blackwood", - "age": 30, - "profession": "Blacksmith", - "personality": "Strong-willed, rugged, and loyal", - "role": "Crafts weapons and tools from unusual metals, recently received an anonymous commission to forge a special item.", - "mystery_intrigue": "Knows the secret smelting process for the indestructible metal, handed down through his family for generations." - } - }, - "player-3": { - "profile": { - "name": "Seraphina Vale", - "age": 28, - "profession": "Historian", - "personality": "Curious, imaginative, and passionate", - "role": "Studies ancient civilizations, recently found a hidden map leading to a forgotten mine.", - "mystery_intrigue": "Possesses a piece of an ancient manuscript that hints at a metal capable of resisting any force, seeks to decipher its meaning." - } - }, - "player-4": { - "profile": { - "name": "Prof. Alfred Mason", - "age": 58, - "profession": "Archaeologist", - "personality": "Analytical, patient, and scholarly", - "role": "Uncovers ancient artifacts, recently discovered a strange metal artifact from a lost civilization.", - "mystery_intrigue": "The artifact he found is made of the same indestructible metal, links to an ancient legend of eternal strength." - } - }, - "player-5": { - "profile": { - "name": "Lila Stone", - "age": 22, - "profession": "Jeweler", - "personality": "Creative, determined, and ambitious", - "role": "Designs and crafts unique jewelry, recently acquired a rare metal to create an exclusive collection.", - "mystery_intrigue": "The rare metal she acquired is the same as the one mentioned in the ancient manuscript, unaware of its true value." - } - }, - "player-6": { - "profile": { - "name": "Det. Noah Thompson", - "age": 45, - "profession": "Detective", - "personality": "Diligent, intuitive, and just", - "role": "Investigates a series of thefts related to rare metals, recently discovered a link to an underground market.", - "mystery_intrigue": "Closely tracking the movements of the indestructible metal, believes it is the key to solving his case." - } - }, - "player-7": { - "profile": { - "name": "Sir Richard LeFevre", - "age": 65, - "profession": "Antiquities Collector", - "personality": "Elegant, shrewd, and eccentric", - "role": "Collects rare and valuable antiques, recently acquired an ancient metal ingot from a mysterious source.", - "mystery_intrigue": "The ingot he possesses is made of the indestructible metal, knows its true worth and the legends surrounding it." - } - } - } - } -} \ No newline at end of file diff --git a/data/b67636b9-08ad-449d-a06a-3d74662e006e/wagons.json b/data/b67636b9-08ad-449d-a06a-3d74662e006e/wagons.json deleted file mode 100644 index 83d177c..0000000 --- a/data/b67636b9-08ad-449d-a06a-3d74662e006e/wagons.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "wagons": [ - { - "id": 0, - "theme": "Tutorial (Start)", - "passcode": "start", - "people": [] - }, - { - "id": 1, - "theme": "minecraft", - "passcode": "Enderdragon", - "people": [ - { - "uid": "wagon-1-player-1", - "position": [ - 0.15, - 0.63 - ], - "rotation": 0.44, - "model_type": "character-female-e", - "items": [] - }, - { - "uid": "wagon-1-player-2", - "position": [ - 0.23, - 0.12 - ], - "rotation": 0.19, - "model_type": "character-male-f", - "items": [] - }, - { - "uid": "wagon-1-player-3", - "position": [ - 0.01, - 0.66 - ], - "rotation": 0.63, - "model_type": "character-male-a", - "items": [] - } - ] - }, - { - "id": 2, - "theme": "minecraft", - "passcode": "Netherite", - "people": [ - { - "uid": "wagon-2-player-1", - "position": [ - 0.94, - 0.89 - ], - "rotation": 0.97, - "model_type": "character-female-e", - "items": [] - }, - { - "uid": "wagon-2-player-2", - "position": [ - 0.21, - 0.35 - ], - "rotation": 0.43, - "model_type": "character-male-b", - "items": [] - }, - { - "uid": "wagon-2-player-3", - "position": [ - 0.25, - 0.82 - ], - "rotation": 0.26, - "model_type": "character-female-a", - "items": [] - }, - { - "uid": "wagon-2-player-4", - "position": [ - 0.39, - 0.16 - ], - "rotation": 0.71, - "model_type": "character-male-a", - "items": [] - }, - { - "uid": "wagon-2-player-5", - "position": [ - 0.39, - 0.7 - ], - "rotation": 0.57, - "model_type": "character-female-b", - "items": [] - }, - { - "uid": "wagon-2-player-6", - "position": [ - 0.62, - 0.96 - ], - "rotation": 0.95, - "model_type": "character-male-c", - "items": [] - }, - { - "uid": "wagon-2-player-7", - "position": [ - 0.18, - 0.25 - ], - "rotation": 0.25, - "model_type": "character-male-d", - "items": [] - } - ] - } - ] -} \ No newline at end of file diff --git a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/names.json b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/names.json new file mode 100644 index 0000000..769b364 --- /dev/null +++ b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/names.json @@ -0,0 +1,45 @@ +{ + "names": { + "wagon-0": {}, + "wagon-1": { + "player-1": { + "firstName": "Dr. Amelia", + "lastName": "Hartley", + "sex": "female", + "fullName": "Dr. Amelia Hartley" + }, + "player-2": { + "firstName": "Thomas", + "lastName": "Rutherford", + "sex": "male", + "fullName": "Thomas Rutherford" + }, + "player-3": { + "firstName": "Maria", + "lastName": "Vasquez", + "sex": "female", + "fullName": "Maria Vasquez" + }, + "player-4": { + "firstName": "Alexei", + "lastName": "Petrov", + "sex": "male", + "fullName": "Alexei Petrov" + } + }, + "wagon-2": { + "player-1": { + "firstName": "Dr. Amelia", + "lastName": "Hartley", + "sex": "female", + "fullName": "Dr. Amelia Hartley" + }, + "player-2": { + "firstName": "Edgar", + "lastName": "Stone", + "sex": "male", + "fullName": "Edgar Stone" + } + } + } +} \ No newline at end of file diff --git a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/player_details.json b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/player_details.json new file mode 100644 index 0000000..780d5db --- /dev/null +++ b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/player_details.json @@ -0,0 +1,69 @@ +{ + "player_details": { + "wagon-0": {}, + "wagon-1": { + "player-1": { + "profile": { + "name": "Dr. Amelia Hartley", + "age": 45, + "profession": "Botanist", + "personality": "Methodical, inquisitive, and reserved", + "role": "Studies rare plants in remote areas, recently discovered a plant with unusual properties.", + "mystery_intrigue": "The plant can emit a strange glow at night, which has piqued the interest of a secret organization. She is wary of the organization's intentions." + } + }, + "player-2": { + "profile": { + "name": "Thomas Rutherford", + "age": 38, + "profession": "Geologist", + "personality": "Analytical, adventurous, and persistent", + "role": "Explores underground formations, recently found an ancient network of tunnels.", + "mystery_intrigue": "The tunnels are filled with symbols resembling the word ''. He believes they are linked to an ancient civilization." + } + }, + "player-3": { + "profile": { + "name": "Maria Vasquez", + "age": 28, + "profession": "Archaeologist", + "personality": "Curious, meticulous, and resourceful", + "role": "Specializes in ancient civilizations, currently studying a series of mysterious artifacts.", + "mystery_intrigue": "The artifacts bear the same symbols found in the tunnels. She suspects they are keys to unlocking a powerful secret." + } + }, + "player-4": { + "profile": { + "name": "Alexei Petrov", + "age": 60, + "profession": "Historian", + "personality": "Wise, scholarly, and introspective", + "role": "Researches the history of ancient civilizations, has a deep knowledge of forgotten languages.", + "mystery_intrigue": "Has deciphered part of the symbols, which hint at a hidden treasure guarded by a ''. He is cautious about sharing his findings." + } + } + }, + "wagon-2": { + "player-1": { + "profile": { + "name": "Dr. Amelia Hartley", + "age": 45, + "profession": "Marine Biologist", + "personality": "Inquisitive, patient, and determined", + "role": "Studies rare underwater creatures and their habitats, recently returned from an expedition to an uncharted island.", + "mystery_intrigue": "Discovered a mysterious pearl with unusual properties, which she has hidden away. The pearl is said to have a magical glow." + } + }, + "player-2": { + "profile": { + "name": "Edgar Stone", + "age": 38, + "profession": "Antique Dealer", + "personality": "Charming, resourceful, and enigmatic", + "role": "Specialized in ancient artifacts and rare collectibles, often travels to exotic locations for his business.", + "mystery_intrigue": "Is searching for a legendary pearl with a unique glow, rumored to possess supernatural powers. Believes Dr. Hartley might have it." + } + } + } + } +} \ No newline at end of file diff --git a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/wagons.json b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/wagons.json new file mode 100644 index 0000000..04a7102 --- /dev/null +++ b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/wagons.json @@ -0,0 +1,84 @@ +{ + "wagons": [ + { + "id": 0, + "theme": "Tutorial (Start)", + "passcode": "start", + "people": [] + }, + { + "id": 1, + "theme": "Minecraft", + "passcode": "Creeper", + "people": [ + { + "uid": "wagon-1-player-1", + "position": [ + 0.8, + 0.15 + ], + "rotation": 0.04, + "model_type": "character-female-e", + "items": [] + }, + { + "uid": "wagon-1-player-2", + "position": [ + 0.9, + 0.44 + ], + "rotation": 0.12, + "model_type": "character-male-e", + "items": [] + }, + { + "uid": "wagon-1-player-3", + "position": [ + 0.75, + 0.77 + ], + "rotation": 0.76, + "model_type": "character-female-a", + "items": [] + }, + { + "uid": "wagon-1-player-4", + "position": [ + 0.74, + 0.06 + ], + "rotation": 0.3, + "model_type": "character-male-a", + "items": [] + } + ] + }, + { + "id": 2, + "theme": "Minecraft", + "passcode": "Enderpearl", + "people": [ + { + "uid": "wagon-2-player-1", + "position": [ + 0.62, + 0.07 + ], + "rotation": 0.17, + "model_type": "character-female-e", + "items": [] + }, + { + "uid": "wagon-2-player-2", + "position": [ + 0.63, + 0.79 + ], + "rotation": 0.67, + "model_type": "character-male-d", + "items": [] + } + ] + } + ] +} \ No newline at end of file From a387c931ac4bdfd93419af6936e9b9fab5fa4401 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Fri, 31 Jan 2025 14:57:29 +0100 Subject: [PATCH 07/11] fix advance_wagon to load new wagon when advancing and update session with current WagonProgress --- app/routes/players.py | 3 -- app/services/chat_service.py | 3 -- app/services/guess_service.py | 38 +++++++++++----- app/services/session_service.py | 80 +++++++++++++++++++++++---------- app/utils/file_management.py | 2 - 5 files changed, 83 insertions(+), 43 deletions(-) diff --git a/app/routes/players.py b/app/routes/players.py index bade5ff..0f78d85 100644 --- a/app/routes/players.py +++ b/app/routes/players.py @@ -84,13 +84,10 @@ async def get_player_info( logger.error("Missing 'names' key in loaded data") raise HTTPException(status_code=404, detail="Names not found") - print("I was also here", names["names"][wagon_id][player_id]) name_info = names["names"][wagon_id][player_id] logger.debug( f"Found name info | wagon: wagon_id | player: player_id" ) - - print("I was here", player_info, name_info) # Combine information complete_player_info = { diff --git a/app/services/chat_service.py b/app/services/chat_service.py index d843db1..d1227c2 100644 --- a/app/services/chat_service.py +++ b/app/services/chat_service.py @@ -47,12 +47,10 @@ def _load_player_details(cls, session) -> Dict: _, player_details, _ = FileManager.load_session_data(session.session_id, session.default_game) if "player_details" not in player_details: - print("Missing 'player_details' key in JSON data") cls.get_logger().error("Missing 'player_details' key in JSON data") return {} details = player_details["player_details"] - print(f"Loaded character details: {details}") cls.get_logger().info( f"Successfully loaded character details. Available wagons: {list(details.keys())}" ) @@ -77,7 +75,6 @@ def _get_character_context(self, uid: str) -> Optional[Dict]: ) # check if the wagon key exists - print(self.player_details, "player_details") if wagon_key not in self.player_details: self.logger.error( f"Wagon {wagon_key} not found in character details. Available wagons: {list(self.player_details.keys())}" diff --git a/app/services/guess_service.py b/app/services/guess_service.py index 17a5873..75fc480 100644 --- a/app/services/guess_service.py +++ b/app/services/guess_service.py @@ -3,7 +3,7 @@ from langchain_core.prompts import PromptTemplate from pydantic import BaseModel, Field from app.models.session import Message -from pydantic import BaseModel, Field +from app.core.logging import LoggerMixin from app.prompts import GUESSING_PROMPT MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY") @@ -16,8 +16,9 @@ class GuessResponse(BaseModel): ) -class GuessingService: +class GuessingService(LoggerMixin): def __init__(self: "GuessingService") -> None: + self.logger.info("Initializing GuessingService") prompt = PromptTemplate.from_template(GUESSING_PROMPT) llm = ( @@ -30,9 +31,12 @@ def __init__(self: "GuessingService") -> None: ) self.chain = prompt | llm + self.logger.info("GuessingService initialized with Mistral LLM") def filter_password(self: "GuessingService", indication: str, password: str) -> str: - return indication.replace(password, "*******") + filtered = indication.replace(password, "*******") + self.logger.debug(f"Filtered password from indication | original_length={len(indication)} | filtered_length={len(filtered)}") + return filtered def generate( self: "GuessingService", @@ -42,15 +46,25 @@ def generate( current_indication: str, password: str, ) -> GuessResponse: + self.logger.info(f"Generating guess | theme={theme} | num_previous_guesses={len(previous_guesses)} | num_previous_indications={len(previous_indications)}") + previous_indications = [message.content for message in previous_indications] + self.logger.debug(f"Processing previous indications | count={len(previous_indications)}") current_indication = self.filter_password(current_indication, password) - - return self.chain.invoke( - { - "previous_guesses": previous_guesses[:3], - "theme": theme, - "previous_indications": previous_indications[:5], - "current_indication": current_indication, - } - ) + + try: + response = self.chain.invoke( + { + "previous_guesses": previous_guesses[:3], + "theme": theme, + "previous_indications": previous_indications[:5], + "current_indication": current_indication, + } + ) + self.logger.info(f"Generated guess successfully | guess={response.guess} | thoughts_length={len(response.thoughts)}") + return response + + except Exception as e: + self.logger.error(f"Failed to generate guess | error={str(e)} | theme={theme}") + raise diff --git a/app/services/session_service.py b/app/services/session_service.py index feee279..ea86f9a 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -9,6 +9,7 @@ ) from app.core.logging import LoggerMixin import uuid +from app.utils.file_management import FileManager # used as dependency injection for the session service @@ -182,37 +183,70 @@ def update_guessing_progress( @classmethod def advance_wagon(cls, session_id: str) -> bool: """Advance to the next wagon""" + cls.get_logger().info(f"Attempting to advance wagon | session_id={session_id}") + + # Get current session session = cls.get_session(session_id) if not session: - cls.get_logger().error( - f"Failed to advance wagon - session not found | session_id: {session_id}" - ) + cls.get_logger().error(f"Failed to advance wagon - session not found | session_id={session_id}") return False - # advance to the next wagon - next_wagon_id = session.current_wagon.wagon_id + 1 - # check if we are out of bounds - if next_wagon_id > 2: # Assuming max_wagon_id is 2 for this example - cls.get_logger().warning( - f"Cannot advance - already at last wagon | session_id: {session_id} | current_wagon: {session.current_wagon.wagon_id}" + current_wagon_id = session.current_wagon.wagon_id + cls.get_logger().debug(f"Current wagon state | session_id={session_id} | current_wagon_id={current_wagon_id}") + + try: + # Load data based on default_game flag + cls.get_logger().debug(f"Loading session data | session_id={session_id} | default_game={session.default_game}") + next_wagon_id = current_wagon_id + 1 + _, _, wagons_data = FileManager.load_session_data(session_id, session.default_game) + wagons = wagons_data["wagons"] + max_wagons = len(wagons) + + # Check if we're at the last wagon + if next_wagon_id > max_wagons - 1: + cls.get_logger().warning( + f"Cannot advance - already at last wagon | session_id={session_id} | current_wagon={current_wagon_id} | max_wagons={max_wagons}" + ) + raise Exception("Cannot advance - already at last wagon") + + cls.get_logger().debug( + f"Wagon progression details | session_id={session_id} | current_wagon={current_wagon_id} | next_wagon={next_wagon_id} | max_wagons={max_wagons}" ) - return False - # Set up next wagon - session.current_wagon = WagonProgress( - wagon_id=next_wagon_id, - theme="New Theme", - password="New Passcode", - ) + # Load current wagon data for the next wagon setup + current_wagon = wagons[next_wagon_id] + + # Set up next wagon + session.current_wagon = WagonProgress( + wagon_id=next_wagon_id, + theme=current_wagon["theme"], + password=current_wagon["passcode"], + ) - # Clean up the previous guesses - session.guessing_progress = GuessingProgress() - cls.update_session(session) + # Reset guessing progress for new wagon + session.guessing_progress = GuessingProgress() + cls.update_session(session) - cls.get_logger().info( - f"Advanced to next wagon | session_id: {session_id} | new_wagon: {next_wagon_id}" - ) - return True + cls.get_logger().info( + f"Successfully advanced to next wagon | session_id={session_id} | previous_wagon={current_wagon_id} | new_wagon={next_wagon_id} | theme={current_wagon['theme']}" + ) + return True + + except FileNotFoundError as e: + cls.get_logger().error( + f"Failed to load session data | session_id={session_id} | error={str(e)} | error_type=FileNotFoundError" + ) + return False + except KeyError as e: + cls.get_logger().error( + f"Invalid wagon data structure | session_id={session_id} | error={str(e)} | error_type=KeyError" + ) + return False + except Exception as e: + cls.get_logger().error( + f"Unexpected error during wagon advancement | session_id={session_id} | error={str(e)} | error_type={type(e).__name__}" + ) + return False @classmethod def cleanup_old_sessions(cls, max_age_hours: int = 24) -> None: diff --git a/app/utils/file_management.py b/app/utils/file_management.py index 400ad45..10d1527 100644 --- a/app/utils/file_management.py +++ b/app/utils/file_management.py @@ -49,7 +49,6 @@ def get_data_directory(cls, session_id: str, default_game: bool) -> Path: def load_session_data(cls, session_id: str, default_game: bool = True) -> tuple[Dict, Dict, Dict]: """Load all data files for a session""" data_dir = cls.get_data_directory(session_id, default_game) - print("Data directory: ", data_dir) if not data_dir.exists(): cls.get_logger().error(f"Data directory not found: {data_dir}") raise FileNotFoundError(f"No data found for session {session_id}") @@ -66,7 +65,6 @@ def load_session_data(cls, session_id: str, default_game: bool = True) -> tuple[ "directory": str(data_dir) } ) - print(player_details, "jasjdfjasdfjasjdfjsa") return names, player_details, wagons except FileNotFoundError as e: From 49e7d049c66f9277910eb9097b67455b31353968 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Fri, 31 Jan 2025 15:14:17 +0100 Subject: [PATCH 08/11] update .gitignore to only keep data default folder inside data and ignore generated trains by sessions. --- .gitignore | 4 + .../names.json | 45 ---------- .../player_details.json | 69 --------------- .../wagons.json | 84 ------------------- 4 files changed, 4 insertions(+), 198 deletions(-) delete mode 100644 data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/names.json delete mode 100644 data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/player_details.json delete mode 100644 data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/wagons.json diff --git a/.gitignore b/.gitignore index 3dd3569..e6c29c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Ignore all files in data folder except default folder +data/* +!data/default/* + # Python __pycache__/ *.py[cod] diff --git a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/names.json b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/names.json deleted file mode 100644 index 769b364..0000000 --- a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/names.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "names": { - "wagon-0": {}, - "wagon-1": { - "player-1": { - "firstName": "Dr. Amelia", - "lastName": "Hartley", - "sex": "female", - "fullName": "Dr. Amelia Hartley" - }, - "player-2": { - "firstName": "Thomas", - "lastName": "Rutherford", - "sex": "male", - "fullName": "Thomas Rutherford" - }, - "player-3": { - "firstName": "Maria", - "lastName": "Vasquez", - "sex": "female", - "fullName": "Maria Vasquez" - }, - "player-4": { - "firstName": "Alexei", - "lastName": "Petrov", - "sex": "male", - "fullName": "Alexei Petrov" - } - }, - "wagon-2": { - "player-1": { - "firstName": "Dr. Amelia", - "lastName": "Hartley", - "sex": "female", - "fullName": "Dr. Amelia Hartley" - }, - "player-2": { - "firstName": "Edgar", - "lastName": "Stone", - "sex": "male", - "fullName": "Edgar Stone" - } - } - } -} \ No newline at end of file diff --git a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/player_details.json b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/player_details.json deleted file mode 100644 index 780d5db..0000000 --- a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/player_details.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "player_details": { - "wagon-0": {}, - "wagon-1": { - "player-1": { - "profile": { - "name": "Dr. Amelia Hartley", - "age": 45, - "profession": "Botanist", - "personality": "Methodical, inquisitive, and reserved", - "role": "Studies rare plants in remote areas, recently discovered a plant with unusual properties.", - "mystery_intrigue": "The plant can emit a strange glow at night, which has piqued the interest of a secret organization. She is wary of the organization's intentions." - } - }, - "player-2": { - "profile": { - "name": "Thomas Rutherford", - "age": 38, - "profession": "Geologist", - "personality": "Analytical, adventurous, and persistent", - "role": "Explores underground formations, recently found an ancient network of tunnels.", - "mystery_intrigue": "The tunnels are filled with symbols resembling the word ''. He believes they are linked to an ancient civilization." - } - }, - "player-3": { - "profile": { - "name": "Maria Vasquez", - "age": 28, - "profession": "Archaeologist", - "personality": "Curious, meticulous, and resourceful", - "role": "Specializes in ancient civilizations, currently studying a series of mysterious artifacts.", - "mystery_intrigue": "The artifacts bear the same symbols found in the tunnels. She suspects they are keys to unlocking a powerful secret." - } - }, - "player-4": { - "profile": { - "name": "Alexei Petrov", - "age": 60, - "profession": "Historian", - "personality": "Wise, scholarly, and introspective", - "role": "Researches the history of ancient civilizations, has a deep knowledge of forgotten languages.", - "mystery_intrigue": "Has deciphered part of the symbols, which hint at a hidden treasure guarded by a ''. He is cautious about sharing his findings." - } - } - }, - "wagon-2": { - "player-1": { - "profile": { - "name": "Dr. Amelia Hartley", - "age": 45, - "profession": "Marine Biologist", - "personality": "Inquisitive, patient, and determined", - "role": "Studies rare underwater creatures and their habitats, recently returned from an expedition to an uncharted island.", - "mystery_intrigue": "Discovered a mysterious pearl with unusual properties, which she has hidden away. The pearl is said to have a magical glow." - } - }, - "player-2": { - "profile": { - "name": "Edgar Stone", - "age": 38, - "profession": "Antique Dealer", - "personality": "Charming, resourceful, and enigmatic", - "role": "Specialized in ancient artifacts and rare collectibles, often travels to exotic locations for his business.", - "mystery_intrigue": "Is searching for a legendary pearl with a unique glow, rumored to possess supernatural powers. Believes Dr. Hartley might have it." - } - } - } - } -} \ No newline at end of file diff --git a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/wagons.json b/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/wagons.json deleted file mode 100644 index 04a7102..0000000 --- a/data/c9a5eabd-f0f9-42f8-8d50-e151105621d0/wagons.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "wagons": [ - { - "id": 0, - "theme": "Tutorial (Start)", - "passcode": "start", - "people": [] - }, - { - "id": 1, - "theme": "Minecraft", - "passcode": "Creeper", - "people": [ - { - "uid": "wagon-1-player-1", - "position": [ - 0.8, - 0.15 - ], - "rotation": 0.04, - "model_type": "character-female-e", - "items": [] - }, - { - "uid": "wagon-1-player-2", - "position": [ - 0.9, - 0.44 - ], - "rotation": 0.12, - "model_type": "character-male-e", - "items": [] - }, - { - "uid": "wagon-1-player-3", - "position": [ - 0.75, - 0.77 - ], - "rotation": 0.76, - "model_type": "character-female-a", - "items": [] - }, - { - "uid": "wagon-1-player-4", - "position": [ - 0.74, - 0.06 - ], - "rotation": 0.3, - "model_type": "character-male-a", - "items": [] - } - ] - }, - { - "id": 2, - "theme": "Minecraft", - "passcode": "Enderpearl", - "people": [ - { - "uid": "wagon-2-player-1", - "position": [ - 0.62, - 0.07 - ], - "rotation": 0.17, - "model_type": "character-female-e", - "items": [] - }, - { - "uid": "wagon-2-player-2", - "position": [ - 0.63, - 0.79 - ], - "rotation": 0.67, - "model_type": "character-male-d", - "items": [] - } - ] - } - ] -} \ No newline at end of file From 618622a5bf7a173240d6030411b37885f6c7e005 Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Sat, 1 Feb 2025 00:07:29 +0100 Subject: [PATCH 09/11] update data structure for names.json, player_details.json and wagons.json both for internal handling also response when generating new train --- app/core/logging.py | 2 +- app/models/train.py | 21 +- app/routes/generate.py | 95 ++------ app/routes/players.py | 121 +++++----- app/services/chat_service.py | 55 ++--- app/services/generate_train/convert.py | 191 ++++++++------- app/services/generate_train/generate_train.py | 72 ++---- app/services/session_service.py | 3 +- app/utils/file_management.py | 19 +- data/default/names.json | 185 +++++++++----- data/default/player_details.json | 219 ++++++++++------- data/default/wagons.json | 226 +++++++----------- 12 files changed, 587 insertions(+), 622 deletions(-) diff --git a/app/core/logging.py b/app/core/logging.py index dc93705..978ab54 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -51,7 +51,7 @@ def setup_logging() -> None: # Configure root logger root_logger = logging.getLogger() - root_logger.setLevel(logging.INFO) + root_logger.setLevel(logging.DEBUG) # Remove any existing handlers root_logger.handlers = [] diff --git a/app/models/train.py b/app/models/train.py index ac67ba2..85bc475 100644 --- a/app/models/train.py +++ b/app/models/train.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from typing import List, Dict +from typing import List class PassengerProfile(BaseModel): name: str @@ -10,12 +10,14 @@ class PassengerProfile(BaseModel): mystery_intrigue: str class PlayerName(BaseModel): + playerId: str firstName: str lastName: str sex: str fullName: str class PlayerDetails(BaseModel): + playerId: str profile: PassengerProfile class Person(BaseModel): @@ -31,16 +33,19 @@ class Wagon(BaseModel): passcode: str people: List[Person] -class Names(BaseModel): - names: Dict[str, Dict[str, PlayerName]] +class WagonNames(BaseModel): + wagonId: str + players: List[PlayerName] + +class WagonPlayerDetails(BaseModel): + wagonId: str + players: List[PlayerDetails] -class PlayerDetailsResponse(BaseModel): - player_details: Dict[str, Dict[str, PlayerDetails]] class WagonsResponse(BaseModel): wagons: List[Wagon] class GenerateTrainResponse(BaseModel): - names: Names - player_details: PlayerDetailsResponse - wagons: WagonsResponse \ No newline at end of file + names: List[WagonNames] + player_details: List[WagonPlayerDetails] + wagons: List[Wagon] \ No newline at end of file diff --git a/app/routes/generate.py b/app/routes/generate.py index eea90d1..0a6666e 100644 --- a/app/routes/generate.py +++ b/app/routes/generate.py @@ -1,10 +1,10 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException from app.services.generate_train.generate_train import GenerateTrainService -from app.services.generate_train.generate_train import GenerateTrainService -from app.models.train import GenerateTrainResponse, Names, PlayerDetailsResponse, WagonsResponse +from app.models.train import GenerateTrainResponse from app.utils.file_management import FileManager from app.core.logging import get_logger from app.services.session_service import SessionService +import json router = APIRouter( @@ -14,77 +14,12 @@ logger = get_logger("generate") -@router.get( - "/train/{session_id}/{number_of_wagons}/{theme}", - response_model=GenerateTrainResponse, - summary="Generate a new train for a session", - description=""" - Generates a new train with the specified number of wagons and theme for a given session. - Each wagon contains: - - A unique passcode related to the theme - - Multiple passengers with generated names and profiles - - Theme-specific details and characteristics - """, - responses={ - 200: { - "description": "Successfully generated train data", - "content": { - "application/json": { - "example": { - "names": { - "names": { - "wagon-1": { - "player-1": {"first_name": "John", "last_name": "Doe"} - } - } - }, - "player_details": { - "player_details": { - "wagon-1": { - "player-1": {"profile": {"age": 30, "occupation": "Engineer"}} - } - } - }, - "wagons": { - "wagons": { - "wagon-1": {"theme": "Space", "passcode": "Nebula"} - } - } - } - } - } - }, - 400: { - "description": "Invalid request parameters", - "content": { - "application/json": { - "example": {"detail": "number_of_wagons must be between 1 and 6"} - } - } - }, - 404: { - "description": "Session not found", - "content": { - "application/json": { - "example": {"detail": "Session not found"} - } - } - }, - 500: { - "description": "Internal server error", - "content": { - "application/json": { - "example": {"detail": "Failed to generate train: Internal error"} - } - } - } - } -) +@router.get("/train/{session_id}/{number_of_wagons}/{theme}") async def get_generated_train( session_id: str, number_of_wagons: str, theme: str -) -> GenerateTrainResponse: +): """ Generate a new train with specified parameters for a session. @@ -110,22 +45,22 @@ async def get_generated_train( raise HTTPException(status_code=400, detail="number_of_wagons cannot exceed 6") try: - generate_train_service = GenerateTrainService() - names, player_details, wagons = generate_train_service.generate_train(theme, number_of_wagons) - FileManager.save_session_data(session_id, names, player_details, wagons) + names_data, player_details_data, wagons_data = generate_train_service.generate_train(theme, number_of_wagons) + + # Save the raw data + FileManager.save_session_data(session_id, names_data, player_details_data, wagons_data) - response = GenerateTrainResponse( - names=Names(names=names["names"]), - player_details=PlayerDetailsResponse(player_details=player_details["player_details"]), - wagons=WagonsResponse(wagons=wagons["wagons"]) - ) + # Construct response with proper schema + response = { + "names": names_data, + "player_details": player_details_data, + "wagons": wagons_data + } logger.info(f"Setting default_game to False | session_id={session_id}") session.default_game = False - # update the central state of the session SessionService.update_session(session) - return response except Exception as e: diff --git a/app/routes/players.py b/app/routes/players.py index 0f78d85..0925288 100644 --- a/app/routes/players.py +++ b/app/routes/players.py @@ -24,26 +24,6 @@ def load_json_file(file_path: str) -> dict: return {} -def filter_player_info(complete_info: dict, properties: List[str] = None) -> dict: - if not properties: - logger.debug("No properties filter applied") - return complete_info - - filtered_info = {} - # Add the id to the filtered info - filtered_info["id"] = complete_info["id"] - valid_properties = {"profile", "name_info"} - - logger.info(f"Filtering properties: {properties}") - for prop in properties: - if prop in valid_properties and prop in complete_info: - filtered_info[prop] = complete_info[prop] - else: - logger.warning(f"Invalid or missing property requested: {prop}") - - return filtered_info - - @router.get("/api/players/{session_id}/{wagon_id}/{player_id}") async def get_player_info( session_id: str, @@ -67,30 +47,48 @@ async def get_player_info( # Load data based on default_game flag names, player_details, _ = FileManager.load_session_data(session_id, session.default_game) + # try to convert the wagon_id to an integer if it is not already an integer + try: + wagon_index = int(wagon_id.split("-")[1]) + except ValueError: + logger.error(f"Invalid wagon_id: {wagon_id}") + raise HTTPException(status_code=404, detail="Invalid wagon_id") try: # First check if player_details is contained in the loaded data - if "player_details" not in player_details: + if len(player_details) == 0: logger.error("Missing 'player_details' key in loaded data") raise HTTPException(status_code=404, detail="Player details not found") - player_info = player_details["player_details"][wagon_id][player_id] + # players is a list of dictionaries, so we need to filter the list for the player_id + player_info = next((player for player in player_details[wagon_index]["players"] if player["playerId"] == player_id), None) + # check if player_info is found + if player_info is None: + logger.error(f"Player info not found | wagon: {wagon_id} | player: {player_id}") + raise HTTPException(status_code=404, detail="Player info not found") + logger.debug( f"Found player info | wagon: {wagon_id} | player: {player_id} | profile_exists: {'profile' in player_info}" ) # first check if names is contained in the loaded data - if "names" not in names: + if len(names) == 0: logger.error("Missing 'names' key in loaded data") raise HTTPException(status_code=404, detail="Names not found") - name_info = names["names"][wagon_id][player_id] + # "players" is a list of dictionaries, so we need to filter the list for the player_id + name_info = next((player for player in names[wagon_index]["players"] if player["playerId"] == player_id), None) + # check if name_info is found + if name_info is None: + logger.error(f"Name info not found | wagon: {wagon_id} | player: {player_id}") + raise HTTPException(status_code=404, detail="Name info not found") + logger.debug( - f"Found name info | wagon: wagon_id | player: player_id" + f"Found name info | wagon: {wagon_id} | player: {player_id}" ) # Combine information - complete_player_info = { + player_in_current_wagon_info = { "id": player_id, "name_info": name_info, "profile": player_info.get("profile", {}) @@ -99,12 +97,11 @@ async def get_player_info( # Filter properties if specified if properties: logger.info( - f"Filtering player info | requested_properties: {properties} | available_properties: {list(complete_player_info.keys())}" + f"Filtering player info | requested_properties: {properties} | available_properties: {list(player_in_current_wagon_info.keys())}" ) - return filter_player_info(complete_player_info, properties) logger.info("Successfully retrieved complete player info") - return complete_player_info + return player_in_current_wagon_info except KeyError as e: logger.error( @@ -141,38 +138,55 @@ async def get_wagon_players( logger.debug( f"Loading session data | session_id={session_id} | default_game={session.default_game}" ) + + # try catch for wagon_index + try: + wagon_index = int(wagon_id.split("-")[1]) + except ValueError: + logger.error(f"Invalid wagon_id: {wagon_id}") + raise HTTPException(status_code=404, detail="Invalid wagon_id") + + wagon_index = int(wagon_id.split("-")[1]) try: # Load data based on default_game flag names, player_details, _ = FileManager.load_session_data(session_id, session.default_game) - # First check if player_details is contained in the loaded data - if "player_details" not in player_details: - logger.error("Missing 'player_details' key in loaded data") + if len(player_details) == 0: + # check if player_details is contained in the loaded data + logger.error("player_details is empty") raise HTTPException(status_code=404, detail="Player details not found") - # Check if wagon exists in player_details - if wagon_id not in player_details["player_details"]: - logger.error(f"Wagon not found | wagon_id={wagon_id}") - raise HTTPException(status_code=404, detail="Wagon not found") + # Check if player details exists for the wagon_index + # should check whether None or empty list + if not player_details[wagon_index]["players"]: + logger.error(f"Player details not found for wagon_index={wagon_index}") + raise HTTPException(status_code=404, detail="Player details not found") - player_info = player_details["player_details"][wagon_id] - logger.debug(f"Found player info | wagon={wagon_id} | player_count={len(player_info)}") + players_in_current_wagon = player_details[wagon_index]["players"] + logger.debug(f"Found player info | wagon={wagon_id} | player_count={len(players_in_current_wagon)}") - # Check if names data exists and is valid - if "names" not in names: - logger.error("Missing 'names' key in loaded data") + # check if names is contained in the loaded data + if len(names) == 0: + logger.error("names is empty") raise HTTPException(status_code=404, detail="Names not found") - - if wagon_id not in names["names"]: - logger.error(f"Wagon names not found | wagon_id={wagon_id}") - raise HTTPException(status_code=404, detail="Wagon names not found") - - name_info = names["names"][wagon_id] - logger.debug(f"Found name info | wagon={wagon_id} | name_count={len(name_info)}") - + + names_in_current_wagon = names[wagon_index] + logger.debug(f"Found name info | wagon={wagon_id} | name_count={len(names_in_current_wagon)}") + + # Create dictionaries for quick lookup by player ID + name_info = { + player["playerId"]: player + for player in names_in_current_wagon["players"] + } + + player_info = { + player["playerId"]: player + for player in players_in_current_wagon + } + # Combine information for all players in the wagon - players_info = [] + players_in_current_wagon_info = [] for player_id in player_info: logger.debug(f"Processing player | wagon={wagon_id} | player={player_id}") complete_info = { @@ -180,11 +194,10 @@ async def get_wagon_players( "name_info": name_info.get(player_id, {}), "profile": player_info[player_id].get("profile", {}) } - filtered_info = filter_player_info(complete_info, properties) - players_info.append(filtered_info) + players_in_current_wagon_info.append(complete_info) - logger.info(f"Successfully retrieved all players | wagon={wagon_id} | player_count={len(players_info)}") - return {"players": players_info} + logger.info(f"Successfully retrieved all players | wagon={wagon_id} | player_count={len(players_in_current_wagon_info)}") + return {"players": players_in_current_wagon_info} except FileNotFoundError as e: logger.error(f"Failed to load session data | error={str(e)} | session_id={session_id}") diff --git a/app/services/chat_service.py b/app/services/chat_service.py index d1227c2..b3ccca5 100644 --- a/app/services/chat_service.py +++ b/app/services/chat_service.py @@ -19,14 +19,10 @@ def __init__(self, session: UserSession): # Load all available character in every wagon. self.player_details: Dict = self._load_player_details(session) - if not self.player_details: - self.logger.error( - "Failed to initialize character details - dictionary is empty" - ) + if len(self.player_details) == 0: + self.logger.error("Failed to initialize player details - array is empty") else: - self.logger.info( - f"Loaded character details for wagons: {list(self.player_details.keys())}" - ) + self.logger.info(f"Loaded player details for wagons: {list(self.player_details)}") # Get the Mistral API key from environment (injected by ECS) mistral_api_key = os.getenv("MISTRAL_API_KEY") @@ -46,21 +42,19 @@ def _load_player_details(cls, session) -> Dict: # Use FileManager to load the default session data _, player_details, _ = FileManager.load_session_data(session.session_id, session.default_game) - if "player_details" not in player_details: + if len(player_details) == 0: cls.get_logger().error("Missing 'player_details' key in JSON data") return {} - details = player_details["player_details"] - cls.get_logger().info( - f"Successfully loaded character details. Available wagons: {list(details.keys())}" - ) - return details + # success for loading player_details + cls.get_logger().info(f"Successfully loaded player details.: {list(player_details)}") + return player_details except FileNotFoundError as e: cls.get_logger().error(f"Failed to load default player details: {str(e)}") return {} except Exception as e: - cls.get_logger().error(f"Failed to load character details: {str(e)}") + cls.get_logger().error(f"Failed to load player details: {str(e)}") return {} def _get_character_context(self, uid: str) -> Optional[Dict]: @@ -69,35 +63,32 @@ def _get_character_context(self, uid: str) -> Optional[Dict]: self.logger.debug(f"Getting character context for uid: {uid}") # "wagon--player-" uid_splitted = uid.split("-") + # try catch for wagon_index + try: + wagon_index = int(uid_splitted[1]) + except ValueError: + self.logger.error(f"Invalid wagon index | uid: {uid} | wagon_index: {uid_splitted[1]}") + return None + wagon_key, player_key = ( - f"wagon-{uid_splitted[1]}", + f"wagon-{wagon_index}", f"player-{uid_splitted[3]}", ) # check if the wagon key exists - if wagon_key not in self.player_details: - self.logger.error( - f"Wagon {wagon_key} not found in character details. Available wagons: {list(self.player_details.keys())}" - ) + if len(self.player_details) == 0: + self.logger.error("Wagon not found in player details") return None - # check if the player key exists - if player_key not in self.player_details[wagon_key]: - self.logger.error( - f"Player {player_key} not found in wagon {wagon_key}. Available players: {list(self.player_details[wagon_key].keys())}" - ) - return None + # find specific player details + specific_player_detais = next((player for player in self.player_details[wagon_index]["players"] if player["playerId"] == player_key), None) - # get the details of the player that belongs to the wagon - character = self.player_details[wagon_key][player_key] self.logger.debug( - f"Retrieved character context | uid: {uid} | wagon: {wagon_key} | player: {player_key} | profession: {character['profile']['profession']}" + f"Retrieved player context | uid: {uid} | wagon: {wagon_key} | player: {player_key} | profession: {specific_player_detais['profile']['profession']}" ) - return character + return specific_player_detais except (KeyError, IndexError) as e: - self.logger.error( - f"Failed to get character context: {str(e)} | uid: {uid} | error: {str(e)} | player_details_keys: {list(self.player_details.keys()) if self.player_details else None}" - ) + self.logger.error(f"Failed to get character context: {str(e)} | uid: {uid} | error: {str(e)} | player_details_keys: {list(self.player_details) if self.player_details else None}") return None def _create_character_prompt(self, character: Dict) -> str: diff --git a/app/services/generate_train/convert.py b/app/services/generate_train/convert.py index 3d8969d..9e71dc1 100644 --- a/app/services/generate_train/convert.py +++ b/app/services/generate_train/convert.py @@ -1,4 +1,8 @@ +from typing import Dict, List, Tuple import random +from app.core.logging import get_logger + +logger = get_logger("convert") def parse_name(full_name): """ @@ -31,7 +35,8 @@ def infer_sex_from_model(model_type): else: return 'unknown' -def convert_wagon_to_three_jsons(wagon_data): +# triggered for one single wagon +def convert_wagon_to_three_jsons(wagon_data: Dict) -> Tuple[Dict, Dict, Dict]: """ Given a single wagon JSON structure like: { @@ -61,101 +66,103 @@ def convert_wagon_to_three_jsons(wagon_data): passcode = wagon_data.get("passcode", "no-passcode") passengers = wagon_data.get("passengers", []) - # 1) Build the "names" object for this wagon - # The final structure should be: {"wagon-N": {"player-1": {...}, "player-2": {...}, ...}} - names_output = {} - wagon_key = f"wagon-{wagon_id}" - names_output[wagon_key] = {} - - # 2) Build the "player_details" object for this wagon - # The final structure: {"wagon-N": {"player-1": { "profile": {...}}, "player-2": {"profile": {...}}}} - player_details_output = {} - player_details_output[wagon_key] = {} - - # 3) Build the "wagons" array entry for this wagon - # Each wagon in the final output is something like: - # { - # "id": wagon_id, - # "theme": "...", - # "passcode": "...", - # "people": [ - # { - # "uid": "wagon-N-player-i", - # "position": [rand, rand], - # "rotation": rand, - # "model_type": "character-female-e", - # "items": [] - # }, ... - # ] - # } - wagon_entry = { - "id": wagon_id, - "theme": theme, - "passcode": passcode, - "people": [] - } + logger.debug(f"Processing wagon conversion | wagon_id={wagon_id} | theme={theme} | num_passengers={len(passengers)}") - # Loop over passengers to fill each part - for i, passenger in enumerate(passengers, start=1): - player_key = f"player-{i}" - full_name = passenger.get("name", "Unknown") - first_name, last_name = parse_name(full_name) - - model_type = passenger.get("characer_model", "character-unknown") - sex = infer_sex_from_model(model_type) - - # 1) Fill names - names_output[wagon_key][player_key] = { - "firstName": first_name, - "lastName": last_name, - "sex": sex, - "fullName": full_name + try: + # 1) Build the "names" object for this wagon + names_entry = { + "wagonId": f"wagon-{wagon_id}", + "players": [] } - # 2) Fill player_details - profile_dict = { - "name": full_name, - "age": passenger.get("age", 0), - "profession": passenger.get("profession", ""), - "personality": passenger.get("personality", ""), - "role": passenger.get("role", ""), - "mystery_intrigue": passenger.get("mystery_intrigue", "") - } - player_details_output[wagon_key][player_key] = { - "profile": profile_dict + # 2) Build the "player_details" object for this wagon + player_details_entry = { + "wagonId": f"wagon-{wagon_id}", + "players": [] } - # 3) Fill wagons "people" - # Random position within [0..1], random rotation in [0..1], items always [] - person_dict = { - "uid": f"wagon-{wagon_id}-player-{i}", - "position": [round(random.random(), 2), round(random.random(), 2)], - "rotation": round(random.random(), 2), - "model_type": model_type, - "items": [] + # 3) Build the "wagon" object + wagon_entry = { + "id": wagon_id, + "theme": theme, + "passcode": passcode, + "people": [] } - wagon_entry["people"].append(person_dict) - - return names_output, player_details_output, wagon_entry - -def convert_and_return_jsons(wagon_data): - names_result, player_details_result, wagons_entry = {}, {}, [] - for wagon in wagon_data: - names_output, player_details_output, wagon_entry = convert_wagon_to_three_jsons(wagon) - names_result.update(names_output) - player_details_result.update(player_details_output) - wagons_entry.append(wagon_entry) - - # 1) The 'names' JSON typically might aggregate multiple wagons, so we embed our single wagon's result: - # For demonstration, just put it as "names": { ...single wagon data... } - all_names = {"names": names_result} - - # 2) The 'player_details' JSON also might aggregate multiple wagons - all_player_details = {"player_details": player_details_result} - - # 3) The 'wagons' JSON is typically an array of wagons. Here, we only have one: - all_wagons = { - "wagons": wagons_entry - } - return all_names, all_player_details, all_wagons \ No newline at end of file + # Process each passenger + for i, passenger in enumerate(passengers, 1): + logger.debug(f"Converting passenger data | wagon_id={wagon_id} | passenger_index={i} | passenger_name={passenger.get('name', 'Unknown')}") + + player_key = f"player-{i}" + name = passenger.get("name", "") + + # Split name into components + name_parts = name.split() + first_name = name_parts[0] if name_parts else "" + last_name = " ".join(name_parts[1:]) if len(name_parts) > 1 else "" + + # Determine sex based on character model + model_type = passenger.get("characer_model", "character-male-a") + sex = "female" if "female" in model_type else "male" + + # Add to names structure + names_entry["players"].append({ + "playerId": player_key, + "firstName": first_name, + "lastName": last_name, + "sex": sex, + "fullName": name + }) + + # Add to player_details structure + profile = { + "name": name, + "age": passenger.get("age", 0), + "profession": passenger.get("profession", ""), + "personality": passenger.get("personality", ""), + "role": passenger.get("role", ""), + "mystery_intrigue": passenger.get("mystery_intrigue", "") + } + player_details_entry["players"].append({"playerId": player_key, "profile": profile}) + + # Add to wagon people structure + person_dict = { + "uid": f"wagon-{wagon_id}-player-{i}", + "position": [round(random.random(), 2), round(random.random(), 2)], + "rotation": round(random.random(), 2), + "model_type": model_type, + "items": [] + } + wagon_entry["people"].append(person_dict) + + logger.debug(f"Completed wagon conversion | wagon_id={wagon_id} | players_processed={len(passengers)}") + return names_entry, player_details_entry, wagon_entry + + except Exception as e: + logger.error(f"Error converting wagon | wagon_id={wagon_id} | error_type={type(e).__name__} | error_msg={str(e)}") + raise + +def convert_and_return_jsons(wagons_data: List[Dict]) -> Tuple[Dict, Dict, Dict]: + """Convert raw wagon data into the three required JSON structures""" + logger.info(f"Starting conversion of wagon data | total_wagons={len(wagons_data)}") + + all_names = [] + all_player_details = [] + all_wagons = [] + + try: + for wagon in wagons_data: + logger.debug(f"Converting wagon | wagon_id={wagon['id']} | theme={wagon['theme']} | num_passengers={len(wagon.get('passengers', []))}") + + names, player_details, wagon_entry = convert_wagon_to_three_jsons(wagon) + + all_names.append(names) + all_player_details.append(player_details) + all_wagons.append(wagon_entry) + + logger.info(f"Successfully converted all wagons | total_names={len(all_names)} | total_player_details={len(all_player_details)} | total_wagons={len(all_wagons)}") + return all_names, all_player_details, all_wagons + + except Exception as e: + logger.error(f"Error converting wagon data | error_type={type(e).__name__} | error_msg={str(e)}") + raise \ No newline at end of file diff --git a/app/services/generate_train/generate_train.py b/app/services/generate_train/generate_train.py index d98e63a..833e0cb 100644 --- a/app/services/generate_train/generate_train.py +++ b/app/services/generate_train/generate_train.py @@ -3,10 +3,9 @@ import json import random from fastapi import HTTPException -from typing import Tuple, Dict, Any +from typing import Tuple, Dict, Any, List from app.core.logging import LoggerMixin from app.services.generate_train.convert import convert_and_return_jsons -from app.core.logging import get_logger class GenerateTrainService(LoggerMixin): @@ -32,17 +31,21 @@ def generate_wagon_passcodes(self, theme: str, num_wagons: int) -> list[str]: self.logger.error(f"Invalid number of wagons requested: {num_wagons}") return "Please provide a valid number of wagons (1-10)." - # Prompt Mistral API to generate a theme and passcodes + # Prompt Mistral API to generate a theme and passcodes prompt = f""" This is a video game about a player trying to reach the locomotive of a train by finding a passcode for each wagon. You are tasked with generating unique passcodes for the wagons based on the theme '{theme}', to make the game more engaging, fun, and with a sense of progression. + You are tasked with generating unique passcodes for the wagons based on the theme '{theme}', + to make the game more engaging, fun, and with a sense of progression, from easiest to hardest. Each password should be unique enough to not be related to each other but still be connected to the theme. Generate {num_wagons} unique and creative passcodes for the wagons. Each passcode must: + Generate exactly {num_wagons} unique and creative passcodes for the wagons. Each passcode must: 1. Be related to the theme. 2. Be unique, interesting, and creative. 3. In one word, letters only (no spaces or special characters). No explanation needed, just the theme and passcodes in a JSON object format. Example: + Example for the theme "Pirates" and 5 passcodes: {{ "theme": "Pirates", "passcodes": ["Treasure", "Rum", "Skull", "Compass", "Anchor"] @@ -195,84 +198,45 @@ def generate_train_json(self, theme: str, num_wagons: int, min_passengers: int = self.logger.error(f"Error in generate_train_json: {e}") raise ValueError(f"Failed to generate train: {str(e)}") - def generate_train(self, theme: str, num_wagons: int) -> Tuple[Dict, Dict, Dict]: + def generate_train(self, theme: str, num_wagons: int) -> Tuple[List, List, List]: """Main method to generate complete train data""" - self.logger.info( - "Starting train generation", - extra={ - "theme": theme, - "num_wagons": num_wagons, - "service": "GenerateTrainService" - } - ) + self.logger.info(f"Starting train generation | theme={theme} | num_wagons={num_wagons} | service=GenerateTrainService") try: # Log attempt to generate train JSON - self.logger.debug( - "Generating train JSON", - extra={ - "theme": theme, - "num_wagons": num_wagons, - "min_passengers": 2, - "max_passengers": 10 - } - ) + self.logger.debug(f"Generating train JSON | theme={theme} | num_wagons={num_wagons} | min_passengers=2 | max_passengers=10") wagons_json = self.generate_train_json(theme, num_wagons, 2, 10) # Log successful JSON generation and parse attempt - self.logger.debug( - "Train JSON generated, parsing to dict", - extra={ - "json_length": len(wagons_json) - } - ) + self.logger.debug(f"Train JSON generated, parsing to dict | json_length={len(wagons_json)}") wagons = json.loads(wagons_json) # Log conversion attempt - self.logger.debug( - "Converting wagon data to final format", - extra={ - "num_wagons": len(wagons) - } - ) + self.logger.debug(f"Converting wagon data to final format | num_wagons={len(wagons)}") all_names, all_player_details, all_wagons = convert_and_return_jsons(wagons) # Log successful generation with summary self.logger.info( - "Train generation completed successfully", - extra={ - "theme": theme, - "total_wagons": len(all_wagons["wagons"]), - "total_names": len(all_names["names"]), - "total_player_details": len(all_player_details["player_details"]) - } + f"Train generation completed successfully | theme={theme} | " + f"total_wagons={len(all_wagons)} | total_names={len(all_names)} | " + f"total_player_details={len(all_player_details)}" ) return all_names, all_player_details, all_wagons except json.JSONDecodeError as e: self.logger.error( - "JSON parsing error in generate_train", - extra={ - "error_type": "JSONDecodeError", - "error_msg": str(e), - "theme": theme, - "num_wagons": num_wagons - } + f"JSON parsing error in generate_train | error_type=JSONDecodeError | " + f"error_msg={str(e)} | theme={theme} | num_wagons={num_wagons}" ) raise HTTPException(status_code=500, detail=f"Failed to parse train JSON: {str(e)}") except Exception as e: self.logger.error( - "Error in generate_train", - extra={ - "error_type": type(e).__name__, - "error_msg": str(e), - "theme": theme, - "num_wagons": num_wagons - } + f"Error in generate_train | error_type={type(e).__name__} | " + f"error_msg={str(e)} | theme={theme} | num_wagons={num_wagons}" ) raise HTTPException(status_code=500, detail=f"Failed to generate train: {str(e)}") \ No newline at end of file diff --git a/app/services/session_service.py b/app/services/session_service.py index ea86f9a..6a820df 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -198,8 +198,7 @@ def advance_wagon(cls, session_id: str) -> bool: # Load data based on default_game flag cls.get_logger().debug(f"Loading session data | session_id={session_id} | default_game={session.default_game}") next_wagon_id = current_wagon_id + 1 - _, _, wagons_data = FileManager.load_session_data(session_id, session.default_game) - wagons = wagons_data["wagons"] + _, _, wagons = FileManager.load_session_data(session_id, session.default_game) max_wagons = len(wagons) # Check if we're at the last wagon diff --git a/app/utils/file_management.py b/app/utils/file_management.py index 10d1527..d46ba91 100644 --- a/app/utils/file_management.py +++ b/app/utils/file_management.py @@ -25,6 +25,7 @@ def create_session_directory(cls, session_id: str) -> Path: def save_session_data(cls, session_id: str, names: Dict, player_details: Dict, wagons: Dict) -> None: """Save the three main data files for a session""" session_dir = cls.create_session_directory(session_id) + logger = cls.get_logger() # Save each file files_to_save = { @@ -36,7 +37,7 @@ def save_session_data(cls, session_id: str, names: Dict, player_details: Dict, w for filename, data in files_to_save.items(): file_path = session_dir / filename cls.save_json(file_path, data) - cls.get_logger().info(f"Saved {filename} for session {session_id}") + logger.info(f"Saved session data | session_id={session_id} | filename={filename} | path={file_path}") @classmethod def get_data_directory(cls, session_id: str, default_game: bool) -> Path: @@ -48,9 +49,11 @@ def get_data_directory(cls, session_id: str, default_game: bool) -> Path: @classmethod def load_session_data(cls, session_id: str, default_game: bool = True) -> tuple[Dict, Dict, Dict]: """Load all data files for a session""" + logger = cls.get_logger() data_dir = cls.get_data_directory(session_id, default_game) + if not data_dir.exists(): - cls.get_logger().error(f"Data directory not found: {data_dir}") + logger.error(f"Data directory not found | session_id={session_id} | directory={data_dir}") raise FileNotFoundError(f"No data found for session {session_id}") try: @@ -58,17 +61,15 @@ def load_session_data(cls, session_id: str, default_game: bool = True) -> tuple[ player_details = cls.load_json(data_dir / "player_details.json") wagons = cls.load_json(data_dir / "wagons.json") - cls.get_logger().info( - f"Loaded session data from {'default' if default_game else 'session'} directory", - extra={ - "session_id": session_id, - "directory": str(data_dir) - } + logger.info( + f"Loaded session data | session_id={session_id} | " + f"source={'default' if default_game else 'session'} | " + f"directory={data_dir}" ) return names, player_details, wagons except FileNotFoundError as e: - cls.get_logger().error(f"Failed to load required files from {data_dir}: {str(e)}") + logger.error(f"Failed to load files | session_id={session_id} | directory={data_dir} | error={str(e)}") raise FileNotFoundError(f"Missing required data files in {data_dir}") @staticmethod diff --git a/data/default/names.json b/data/default/names.json index 2ccbf7e..183ce48 100644 --- a/data/default/names.json +++ b/data/default/names.json @@ -1,215 +1,270 @@ -{ - "names": { - "wagon-0" : { - - }, - "wagon-1": { - "player-1": { - "firstName": "Victor", +[ + { + "wagonId": "wagon-0", + "players": [] + }, + { + "wagonId": "wagon-1", + "players": [ + { + "playerId": "player-1", + "firstName": "James", "lastName": "Sterling", "sex": "male", - "fullName": "Victor Sterling" + "fullName": "James Sterling" }, - "player-2": { - "firstName": "Isabella", - "lastName": "Hart", + { + "playerId": "player-2", + "firstName": "Elizabeth", + "lastName": "Chen", "sex": "female", - "fullName": "Isabella Hart" + "fullName": "Elizabeth Chen" }, - "player-3": { - "firstName": "Lucas", - "lastName": "Ford", + { + "playerId": "player-3", + "firstName": "Marcus", + "lastName": "Thompson", "sex": "male", - "fullName": "Lucas Ford" + "fullName": "Marcus Thompson" }, - "player-4": { - "firstName": "Eleanor", - "lastName": "Brooks", + { + "playerId": "player-4", + "firstName": "Sofia", + "lastName": "Rodriguez", "sex": "female", - "fullName": "Eleanor Brooks" + "fullName": "Sofia Rodriguez" } - }, - "wagon-2": { - "player-1": { + ] + }, + { + "wagonId": "wagon-2", + "players": [ + { + "playerId": "player-1", "firstName": "Adrian", "lastName": "North", "sex": "male", "fullName": "Eleanor Brooks" }, - "player-2": { + { + "playerId": "player-2", "firstName": "Elena", "lastName": "Stone", "sex": "female", "fullName": "Elena Stone" }, - "player-3": { + { + "playerId": "player-3", "firstName": "Lucas", "lastName": "West", "sex": "male", "fullName": "Lucas West" }, - "player-4": { + { + "playerId": "player-4", "firstName": "Sophia", "lastName": "East", "sex": "female", "fullName": "Sophia East" } - }, - "wagon-3": { - "player-1": { + ] + }, + { + "wagonId": "wagon-3", + "players": [ + { + "playerId": "player-1", "firstName": "Captain Elara", "lastName": "Voss", "sex": "female", "fullName": "Captain Elara Voss" }, - "player-2": { + { + "playerId": "player-2", "firstName": "Dr. Orion", "lastName": "Kane", "sex": "male", "fullName": "Dr. Orion Kane" }, - "player-3": { + { + "playerId": "player-3", "firstName": "Lieutenant Nova", "lastName": "Sterling", "sex": "female", "fullName": "Lieutenant Nova Sterling" }, - "player-4": { + { + "playerId": "player-4", "firstName": "Professor Atlas", "lastName": "Grey", "sex": "male", "fullName": "Professor Atlas Grey" } - }, - "wagon-4": { - "player-1": { + ] + }, + { + "wagonId": "wagon-4", + "players": [ + { + "playerId": "player-1", "firstName": "General Victor", "lastName": "Blackwood", "sex": "male", "fullName": "General Victor Blackwood" }, - "player-2": { + { + "playerId": "player-2", "firstName": "Lady Isabella", "lastName": "Sterling", "sex": "female", "fullName": "Lady Isabella Sterling" }, - "player-3": { + { + "playerId": "player-3", "firstName": "Sergeant Marcus", "lastName": "Thorne", "sex": "male", "fullName": "Noah Davis" }, - "player-4": { + { + "playerId": "player-4", "firstName": "Dr. Charlotte", "lastName": "Hartley", "sex": "female", "fullName": "Dr. Charlotte Hartley" } - }, - "wagon-5": { - "player-1": { + ] + }, + { + "wagonId": "wagon-5", + "players": [ + { + "playerId": "player-1", "firstName": "Maestro Lorenzo", "lastName": "Rossi", "sex": "male", "fullName": "Maestro Lorenzo Rossi" }, - "player-2": { + { + "playerId": "player-2", "firstName": "Isabella", "lastName": "Valentina", "sex": "female", "fullName": "Isabella Valentina" }, - "player-3": { + { + "playerId": "player-3", "firstName": "Leonardo", "lastName": "Di Marco", "sex": "male", "fullName": "Leonardo Di Marco" }, - "player-4": { + { + "playerId": "player-4", "firstName": "Sofia", "lastName": "Caruso", "sex": "female", "fullName": "Sofia Caruso" } - }, - "wagon-6": { - "player-1": { + ] + }, + { + "wagonId": "wagon-6", + "players": [ + { + "playerId": "player-1", "firstName": "Leo 'Roar'", "lastName": "Johnson", "sex": "male", "fullName": "Leo 'Roar' Johnson" }, - "player-2": { + { + "playerId": "player-2", "firstName": "Lila 'Nightingale'", "lastName": "Silva", "sex": "female", "fullName": "Lady Olivia Wright" }, - "player-3": { + { + "playerId": "player-3", "firstName": "Rafael 'Panther'", "lastName": "Martinez", "sex": "male", "fullName": "Rafael 'Panther' Martinez" }, - "player-4": { + { + "playerId": "player-4", "firstName": "Elena 'Jaguar'", "lastName": "Rodriguez", "sex": "female", "fullName": "Elena 'Jaguar' Rodriguez" } - }, - "wagon-7": { - "player-1": { + ] + }, + { + "wagonId": "wagon-7", + "players": [ + { + "playerId": "player-1", "firstName": "Marco", "lastName": "Valentino", "sex": "male", "fullName": "Marco Valentino" }, - "player-2": { + { + "playerId": "player-2", "firstName": "Giovanna", "lastName": "Rosalia", "sex": "female", "fullName": "Giovanna Rosalia" }, - "player-3": { + { + "playerId": "player-3", "firstName": "Father", "lastName": "Lorenzo", "sex": "male", "fullName": "Father Lorenzo" }, - "player-4": { + { + "playerId": "player-4", "firstName": "Luca", "lastName": "Montefiori", "sex": "male", "fullName": "Luca Montefiori" } - }, - "wagon-8": { - "player-1": { + ] + }, + { + "wagonId": "wagon-8", + "players": [ + { + "playerId": "player-1", "firstName": "Eleanor", "lastName": "Voss", "sex": "male", "fullName": "Eleanor Voss" }, - "player-2": { + { + "playerId": "player-2", "firstName": "Theadore", "lastName": "Kane", "sex": "female", "fullName": "Theadore Kane" }, - "player-3": { + { + "playerId": "player-3", "firstName": "Victoria", "lastName": "Sterling", "sex": "female", "fullName": "Victoria Sterling" }, - "player-4": { + { + "playerId": "player-4", "firstName": "Edgar", "lastName": "Grey", "sex": "male", "fullName": "Edgar Grey" } - } + ] } -} \ No newline at end of file +] diff --git a/data/default/player_details.json b/data/default/player_details.json index 9947265..6a3965d 100644 --- a/data/default/player_details.json +++ b/data/default/player_details.json @@ -1,52 +1,62 @@ -{ - "player_details": { - "wagon-0": {}, - - "wagon-1": { - "player-1": { - "profile": { - "name": "Victor Sterling", - "age": 55, - "profession": "Mining Magnate", - "personality": "Ambitious, cunning, and charismatic", - "role": "Owns a vast mining empire, recently discovered a new vein of precious metal with a peculiar luster.", - "mystery_intrigue": "Secretly trades in unregistered precious metals, hiding a fortune in a secure vault." +[ + { + "wagonId": "wagon-0", + "players": [] + }, + { + "wagonId": "wagon-1", + "players": [ + { + "playerId": "player-1", + "profile": { + "name": "James Sterling", + "age": 42, + "profession": "Investment Banker", + "personality": "Ambitious and calculating", + "role": "A successful banker with questionable ethics", + "mystery_intrigue": "Has been manipulating gold prices for personal gain" } }, - "player-2": { + { + "playerId": "player-2", "profile": { - "name": "Isabella Hart", - "age": 38, - "profession": "Jewelry Designer", - "personality": "Creative, passionate, and meticulous", - "role": "Designs exquisite jewelry for the elite, recently commissioned to create pieces from a rare, gleaming metal.", - "mystery_intrigue": "Receives anonymous shipments of raw, lustrous metal, unaware of its true origin." + "name": "Elizabeth Chen", + "age": 35, + "profession": "Financial Analyst", + "personality": "Sharp-minded and detail-oriented", + "role": "Whistleblower investigating financial fraud", + "mystery_intrigue": "Has evidence of major market manipulation" } }, - "player-3": { + { + "playerId": "player-3", "profile": { - "name": "Lucas Ford", - "age": 42, - "profession": "Financial Advisor", - "personality": "Analytical, cautious, and strategic", - "role": "Manages the investments of wealthy clients, notices unusual transactions involving large amounts of gleaming bullion.", - "mystery_intrigue": "Suspects a client of illegal trading in unregistered precious metals, but lacks concrete evidence." + "name": "Marcus Thompson", + "age": 45, + "profession": "Securities Regulator", + "personality": "Stern and methodical", + "role": "Investigating suspicious trading patterns", + "mystery_intrigue": "Close to uncovering a major financial scandal" } }, - "player-4": { + { + "playerId": "player-4", "profile": { - "name": "Eleanor Brooks", - "age": 32, - "profession": "Investigative Journalist", - "personality": "Tenacious, curious, and ethical", - "role": "Investigates corruption in the mining industry, follows a lead on a hidden stash of radiant metal bars.", - "mystery_intrigue": "Uncovers a network of illegal precious metal trades, putting her life in danger." + "name": "Sofia Rodriguez", + "age": 38, + "profession": "Corporate Lawyer", + "personality": "Shrewd and diplomatic", + "role": "Legal counsel involved in the investigation", + "mystery_intrigue": "May be working for both sides" } } - }, - - "wagon-2": { - "player-1": { + ] + }, + { + "wagonId": "wagon-2", + "players": [ + { + "playerId": "player-1", "profile": { "name": "Adrian North", "age": 38, @@ -56,7 +66,8 @@ "mystery_intrigue": "Carries a mysterious map that seems to guide them towards a hidden treasure, but the map has strange symbols that no one can decipher." } }, - "player-2": { + { + "playerId": "player-2", "profile": { "name": "Elena Stone", "age": 32, @@ -66,7 +77,8 @@ "mystery_intrigue": "Has discovered clues suggesting the existence of a lost civilization, but keeps this information secret from the team." } }, - "player-3": { + { + "playerId": "player-3", "profile": { "name": "Lucas West", "age": 28, @@ -76,7 +88,8 @@ "mystery_intrigue": "Finds a rare plant that has healing properties, but its location is marked by a peculiar symbol that resembles a directional pointer." } }, - "player-4": { + { + "playerId": "player-4", "profile": { "name": "Sophia East", "age": 35, @@ -86,10 +99,13 @@ "mystery_intrigue": "Uncovers a hidden language that seems to provide directions to a mysterious artifact, but the language is unlike anything she has ever seen." } } - }, - - "wagon-3": { - "player-1": { + ] + }, + { + "wagonId": "wagon-3", + "players": [ + { + "playerId": "player-1", "profile": { "name": "Captain Elara Voss", "age": 40, @@ -99,7 +115,8 @@ "mystery_intrigue": "Hiding a personal vendetta against a rival exploration company, which may influence her decisions and put the crew in danger." } }, - "player-2": { + { + "playerId": "player-2", "profile": { "name": "Dr. Orion Kane", "age": 35, @@ -109,7 +126,8 @@ "mystery_intrigue": "Has discovered a hidden artifact that could revolutionize their understanding of the underwater civilization, but keeps it secret." } }, - "player-3": { + { + "playerId": "player-3", "profile": { "name": "Lieutenant Nova Sterling", "age": 30, @@ -119,7 +137,8 @@ "mystery_intrigue": "Receiving encrypted messages from an unknown source, warning her about an impending danger within the crew." } }, - "player-4": { + { + "playerId": "player-4", "profile": { "name": "Professor Atlas Grey", "age": 45, @@ -129,10 +148,13 @@ "mystery_intrigue": "Has uncovered evidence of a dangerous underwater creature that could threaten the expedition, but keeps this information to himself." } } - }, - - "wagon-4": { - "player-1": { + ] + }, + { + "wagonId": "wagon-4", + "players": [ + { + "playerId": "player-1", "profile": { "name": "General Victor Blackwood", "age": 55, @@ -142,7 +164,8 @@ "mystery_intrigue": "Haunted by a past decision that led to a devastating defeat, seeks redemption and closure." } }, - "player-2": { + { + "playerId": "player-2", "profile": { "name": "Lady Isabella Sterling", "age": 40, @@ -152,7 +175,8 @@ "mystery_intrigue": "Discovers letters hinting at a secret alliance that could have changed the course of the battle, but keeps this information hidden." } }, - "player-3": { + { + "playerId": "player-3", "profile": { "name": "Sergeant Marcus Thorne", "age": 38, @@ -162,7 +186,8 @@ "mystery_intrigue": "Experiences vivid dreams of the battle, revealing clues about a hidden treasure, but fears being seen as unstable." } }, - "player-4": { + { + "playerId": "player-4", "profile": { "name": "Dr. Charlotte Hartley", "age": 35, @@ -172,10 +197,13 @@ "mystery_intrigue": "Uncovers evidence of a cover-up involving high-ranking officers, but struggles with the ethical implications of revealing the truth." } } - }, - - "wagon-5": { - "player-1": { + ] + }, + { + "wagonId": "wagon-5", + "players": [ + { + "playerId": "player-1", "profile": { "name": "Maestro Lorenzo Rossi", "age": 58, @@ -185,7 +213,8 @@ "mystery_intrigue": "Hides a rare, unpublished manuscript that could redefine musical history, but fears the controversy it might cause." } }, - "player-2": { + { + "playerId": "player-2", "profile": { "name": "Isabella Valentina", "age": 32, @@ -195,7 +224,8 @@ "mystery_intrigue": "Possesses an antique violin with a mysterious inscription, hinting at a hidden composition of immense significance." } }, - "player-3": { + { + "playerId": "player-3", "profile": { "name": "Leonardo Di Marco", "age": 45, @@ -205,7 +235,8 @@ "mystery_intrigue": "Discovers a lost symphony that echoes the genius of past masters, but grapples with the ethical dilemma of claiming it as his own." } }, - "player-4": { + { + "playerId": "player-4", "profile": { "name": "Sofia Caruso", "age": 28, @@ -215,10 +246,13 @@ "mystery_intrigue": "Finds a hidden letter revealing a secret affair between two famous musicians, which could scandalize the musical world." } } - }, - - "wagon-6": { - "player-1": { + ] + }, + { + "wagonId": "wagon-6", + "players": [ + { + "playerId": "player-1", "profile": { "name": "Leo 'Roar' Johnson", "age": 42, @@ -228,7 +262,8 @@ "mystery_intrigue": "Possesses a rare, antique saxophone with engravings of a mysterious jungle creature, hinting at a hidden treasure deep within the rainforest." } }, - "player-2": { + { + "playerId": "player-2", "profile": { "name": "Lila 'Nightingale' Silva", "age": 35, @@ -238,7 +273,8 @@ "mystery_intrigue": "Discovers an old map leading to a legendary jungle temple, guarded by a mythical beast, but keeps the map's existence a secret." } }, - "player-3": { + { + "playerId": "player-3", "profile": { "name": "Rafael 'Panther' Martinez", "age": 38, @@ -248,7 +284,8 @@ "mystery_intrigue": "Receives mysterious drum patterns in his dreams, which he believes are clues to an ancient jungle ritual, but fears the consequences of unleashing its power." } }, - "player-4": { + { + "playerId": "player-4", "profile": { "name": "Elena 'Jaguar' Rodriguez", "age": 30, @@ -258,10 +295,13 @@ "mystery_intrigue": "Finds a hidden journal detailing a legendary jungle expedition, but the journal's warnings of a dangerous predator make her hesitant to reveal its contents." } } - }, - - "wagon-7": { - "player-1": { + ] + }, + { + "wagonId": "wagon-7", + "players": [ + { + "playerId": "player-1", "profile": { "name": "Marco Valentino", "age": 48, @@ -271,7 +311,8 @@ "mystery_intrigue": "Haunted by a past betrayal that deepened the feud, seeks revenge while hiding a secret that could unite the families." } }, - "player-2": { + { + "playerId": "player-2", "profile": { "name": "Giovanna Rosalia", "age": 42, @@ -281,7 +322,8 @@ "mystery_intrigue": "Discovers an old love letter hinting at a forbidden romance between members of the rival families, but struggles with the implications of revealing it." } }, - "player-3": { + { + "playerId": "player-3", "profile": { "name": "Father Lorenzo", "age": 50, @@ -291,7 +333,8 @@ "mystery_intrigue": "Knows of a hidden sanctuary where the lovers can meet, but fears the consequences of defying the powerful families." } }, - "player-4": { + { + "playerId": "player-4", "profile": { "name": "Luca Montefiori", "age": 35, @@ -301,10 +344,13 @@ "mystery_intrigue": "Uncovers a plot to sabotage the upcoming wedding, which could reignite the feud and plunge the city into chaos." } } - }, - - "wagon-8": { - "player-1": { + ] + }, + { + "wagonId": "wagon-8", + "players": [ + { + "playerId": "player-1", "profile": { "name": "Eleanor Voss", "age": 38, @@ -314,7 +360,8 @@ "mystery_intrigue": "Discovers hidden recordings on an old cylinder that hint at a secret society communicating through coded messages." } }, - "player-2": { + { + "playerId": "player-2", "profile": { "name": "Theodore Kane", "age": 42, @@ -324,7 +371,8 @@ "mystery_intrigue": "Finds blueprints for an advanced device that can record and play back voices with unprecedented clarity, but the inventor's identity remains a mystery." } }, - "player-3": { + { + "playerId": "player-3", "profile": { "name": "Victoria Sterling", "age": 35, @@ -334,7 +382,8 @@ "mystery_intrigue": "Receives anonymous letters containing cryptic messages and musical notes, hinting at a hidden melody with mysterious powers." } }, - "player-4": { + { + "playerId": "player-4", "profile": { "name": "Edgar Grey", "age": 40, @@ -344,6 +393,6 @@ "mystery_intrigue": "Uncovers a plot to steal advanced sound recording technology, but struggles to identify the mastermind behind the scheme." } } - } + ] } -} +] diff --git a/data/default/wagons.json b/data/default/wagons.json index 4d8535e..8c65d8c 100644 --- a/data/default/wagons.json +++ b/data/default/wagons.json @@ -1,109 +1,80 @@ -{ - "wagons": [ - { - "id": 0, - "theme": "Tutorial (Start)", - "passcode": "start", - "people": [] - }, - { - "id": 1, - "theme": "A Flourishing Business", - "passcode": "gold", - "people": [ - { - "uid": "wagon-1-player-1", - "position": [ - 0.5, - 0.5 - ], - "rotation": 0, - "model_type": "character-male-e", - "items": [] - }, - { - "uid": "wagon-1-player-2", - "position": [ - 0.25, - 0.25 - ], - "rotation": 0, - "model_type": "character-female-f", - "items": [] - }, - { - "uid": "wagon-1-player-3", - "position": [ - 0.75, - 0.55 - ], - "rotation": 0, - "model_type": "character-male-c", - "items": [] - }, - { - "uid": "wagon-1-player-4", - "position": [ - 0.75, - 0.65 - ], - "rotation": 0, - "model_type": "character-female-e", - "items": [] - } - ] - }, - { - "id": 2, - "theme": "Off the Map", - "passcode": "aztec", - "people": [ - { - "uid": "wagon-2-player-1", - "position": [ - 0.5, - 0.5 - ], - "rotation": 0, - "model_type": "character-male-e", - "items": [] - }, - { - "uid": "wagon-2-player-2", - "position": [ - 0.25, - 0.25 - ], - "rotation": 0, - "model_type": "character-female-e", - "items": [ - "blaster", - "cone" - ] - }, - { - "uid": "wagon-2-player-3", - "position": [ - 0.75, - 0.55 - ], - "rotation": 0, - "model_type": "character-male-e", - "items": [] - }, - { - "uid": "wagon-2-player-4", - "position": [ - 0.75, - 0.65 - ], - "rotation": 0, - "model_type": "character-female-f", - "items": [] - } - ] - } - , +[ + { + "id": 0, + "theme": "Tutorial (Start)", + "passcode": "start", + "people": [] + }, + { + "id": 1, + "theme": "A Flourishing Business", + "passcode": "gold", + "people": [ + { + "uid": "wagon-1-player-1", + "position": [0.5, 0.5], + "rotation": 0, + "model_type": "character-male-e", + "items": [] + }, + { + "uid": "wagon-1-player-2", + "position": [0.25, 0.25], + "rotation": 0, + "model_type": "character-female-f", + "items": [] + }, + { + "uid": "wagon-1-player-3", + "position": [0.75, 0.55], + "rotation": 0, + "model_type": "character-male-c", + "items": [] + }, + { + "uid": "wagon-1-player-4", + "position": [0.75, 0.65], + "rotation": 0, + "model_type": "character-female-e", + "items": [] + } + ] + }, + { + "id": 2, + "theme": "Off the Map", + "passcode": "aztec", + "people": [ + { + "uid": "wagon-2-player-1", + "position": [0.5, 0.5], + "rotation": 0, + "model_type": "character-male-e", + "items": [] + }, + { + "uid": "wagon-2-player-2", + "position": [0.25, 0.25], + "rotation": 0, + "model_type": "character-female-e", + "items": ["blaster", "cone"] + }, + { + "uid": "wagon-2-player-3", + "position": [0.75, 0.55], + "rotation": 0, + "model_type": "character-male-e", + "items": [] + }, + { + "uid": "wagon-2-player-4", + "position": [0.75, 0.65], + "rotation": 0, + "model_type": "character-female-f", + "items": [] + } + ] + }, { "id": 3, "theme": "Ving Mille Lieues Sous Les Mers", @@ -111,40 +82,28 @@ "people": [ { "uid": "wagon-3-player-1", - "position": [ - 0.5, - 0.5 - ], + "position": [0.5, 0.5], "rotation": 0, "model_type": "character-female-d", "items": [] }, { "uid": "wagon-3-player-2", - "position": [ - 0.25, - 0.25 - ], + "position": [0.25, 0.25], "rotation": 0, "model_type": "character-male-d", "items": [] }, { "uid": "wagon-3-player-3", - "position": [ - 0.75, - 0.55 - ], + "position": [0.75, 0.55], "rotation": 0, "model_type": "character-female-b", "items": [] }, { "uid": "wagon-3-player-4", - "position": [ - 0.75, - 0.65 - ], + "position": [0.75, 0.65], "rotation": 0, "model_type": "character-male-c", "items": [] @@ -158,40 +117,28 @@ "people": [ { "uid": "wagon-4-player-1", - "position": [ - 0.5, - 0.5 - ], + "position": [0.5, 0.5], "rotation": 0, "model_type": "character-male-a", "items": [] }, { "uid": "wagon-4-player-2", - "position": [ - 0.25, - 0.25 - ], + "position": [0.25, 0.25], "rotation": 0, "model_type": "character-female-a", "items": [] }, { "uid": "wagon-4-player-3", - "position": [ - 0.75, - 0.55 - ], + "position": [0.75, 0.55], "rotation": 0, "model_type": "character-male-f", "items": [] }, { "uid": "wagon-4-player-4", - "position": [ - 0.75, - 0.65 - ], + "position": [0.75, 0.65], "rotation": 0, "model_type": "character-female-b", "items": [] @@ -338,5 +285,4 @@ } ] } - ] -} \ No newline at end of file +] From 6dbb18fad4cfbea5cb2c66865e5f0ec4e958852c Mon Sep 17 00:00:00 2001 From: "Martin, Mohammed (ext) (CYS GRS ARC)" <“mohammed.martin.ext@siemens.com”> Date: Sat, 1 Feb 2025 00:17:50 +0100 Subject: [PATCH 10/11] update the prompt in generate_passengers_for_wagon + adding theme string parameter to function --- app/services/generate_train/generate_train.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/services/generate_train/generate_train.py b/app/services/generate_train/generate_train.py index 833e0cb..2cacaa8 100644 --- a/app/services/generate_train/generate_train.py +++ b/app/services/generate_train/generate_train.py @@ -74,13 +74,14 @@ def generate_wagon_passcodes(self, theme: str, num_wagons: int) -> list[str]: self.logger.error(f"Error generating passcodes: {e}") return f"Error generating passcodes: {str(e)}" - def generate_passengers_for_wagon(self, passcode: str, num_passengers: int) -> list[Dict[str, Any]]: + def generate_passengers_for_wagon(self, theme: str, passcode: str, num_passengers: int) -> list[Dict[str, Any]]: """Generate passengers for a wagon using Mistral AI""" - self.logger.info(f"Generating {num_passengers} passengers for wagon with passcode: {passcode}") + self.logger.info(f"Generating {num_passengers} passengers for wagon with passcode: {passcode} and theme: {theme}") # Generate passengers with the Mistral API prompt = f""" Passengers are in a wagon. The player can interact with them to learn more about their stories. + The passengers live in the world of the theme "{theme}" and their stories are connected to the passcode "{passcode}". The following is a list of passengers on a train wagon. The wagon is protected by the passcode "{passcode}". Their stories are intertwined, and each passenger has a unique role and mystery, all related to the theme and the passcode. The player must be able to guess the passcode by talking to the passengers and uncovering their secrets. @@ -113,7 +114,6 @@ def generate_passengers_for_wagon(self, passcode: str, num_passengers: int) -> l - character-male-e: Brown-haired man with glasses, wearing a white lab coat and a yellow tool belt (scientist, mechanic, or engineer). - character-male-f: Dark-haired young man with a mustache, wearing a green vest and brown pants (possibly an explorer, traveler, or adventurer). Generate {num_passengers} passengers in JSON array format. Example: - [ {{ "name": "Victor Sterling", @@ -134,7 +134,6 @@ def generate_passengers_for_wagon(self, passcode: str, num_passengers: int) -> l "characer_model": "character-female-f" }} ] - Now generate the JSON array: """ response = self.client.chat.complete( @@ -184,7 +183,7 @@ def generate_train_json(self, theme: str, num_wagons: int, min_passengers: int = }) for i, passcode in enumerate(passcodes): num_passengers = random.randint(min_passengers, max_passengers) - passengers = self.generate_passengers_for_wagon(passcode, num_passengers) + passengers = self.generate_passengers_for_wagon(theme, passcode, num_passengers) # Check if passengers is a string (error message) if isinstance(passengers, str): self.logger.error(f"Error generating passengers: {passengers}") From 7d095380a5a9ffabb8ecb0816d312d7745a736ad Mon Sep 17 00:00:00 2001 From: OrdinaryDev83 Date: Sat, 1 Feb 2025 00:21:04 +0100 Subject: [PATCH 11/11] feat: character prompts contain theme --- app/routes/chat.py | 2 +- app/services/chat_service.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/routes/chat.py b/app/routes/chat.py index 3e45114..f45adf9 100644 --- a/app/routes/chat.py +++ b/app/routes/chat.py @@ -162,7 +162,7 @@ async def chat_with_character( if not conversation: raise HTTPException(status_code=500, detail="Failed to process message") - ai_response = chat_service.generate_response(uid, conversation) + ai_response = chat_service.generate_response(uid, session.current_wagon.theme, conversation) if not ai_response: raise HTTPException(status_code=500, detail="Failed to generate response") diff --git a/app/services/chat_service.py b/app/services/chat_service.py index b3ccca5..78900c8 100644 --- a/app/services/chat_service.py +++ b/app/services/chat_service.py @@ -91,7 +91,7 @@ def _get_character_context(self, uid: str) -> Optional[Dict]: self.logger.error(f"Failed to get character context: {str(e)} | uid: {uid} | error: {str(e)} | player_details_keys: {list(self.player_details) if self.player_details else None}") return None - def _create_character_prompt(self, character: Dict) -> str: + def _create_character_prompt(self, theme: str, character: Dict) -> str: """Create a prompt that describes the character's personality and context""" occupation = character["profile"]["profession"] personality = character["profile"]["personality"] @@ -100,7 +100,8 @@ def _create_character_prompt(self, character: Dict) -> str: name = character["profile"]["name"] prompt = f""" - You are an NPC in a fictional world. Your name is {name}, and you are a {occupation} by trade. + You are an NPC in a fictional world set in the theme of {theme}. You are part of this theme's story and lore. + Your name is {name}, and you are a {occupation}. Your role in the story is {role}, and you have a mysterious secret tied to you: {mystery}. Your personality is {personality}, which influences how you speak, act, and interact with others. Stay in character at all times, and respond to the player based on your occupation, role, mystery, and personality. @@ -116,7 +117,7 @@ def _create_character_prompt(self, character: Dict) -> str: return prompt - def generate_response(self, uid: str, conversation: Conversation) -> Optional[str]: + def generate_response(self, uid: str, theme: str, conversation: Conversation) -> Optional[str]: """Generate a response using Mistral AI based on character profile""" self.logger.info(f"Generating response for uid: {uid}") character = self._get_character_context(uid) @@ -129,7 +130,7 @@ def generate_response(self, uid: str, conversation: Conversation) -> Optional[st try: # Create the system prompt with character context - system_prompt = self._create_character_prompt(character) + system_prompt = self._create_character_prompt(theme, character) # Convert conversation history to Mistral AI format messages = [{"role": "system", "content": system_prompt}]