Skip to content
Merged

Dev #31

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Ignore all files in data folder except default folder
data/*
!data/default/*

# Python
__pycache__/
*.py[cod]
Expand Down
102 changes: 56 additions & 46 deletions app/core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.INFO)

# 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.DEBUG)

# 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()
return get_logger(self.__class__.__name__)
23 changes: 17 additions & 6 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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.core.logging import get_logger
from app.routes import health, wagons, chat, players, generate
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()
Expand All @@ -26,7 +26,7 @@
# 1. Configure CORS for Unity Web Requests
# -----------------------------------------------------------------------------
#
# Unitys 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",
Expand All @@ -36,11 +36,11 @@
# 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 thats up to your application needs.
# Note: We also allow PUT, DELETE, etc., but that's up to your application needs.
#
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # "https://your-custom-domain.com" if credentials are needed
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
Expand Down Expand Up @@ -155,6 +155,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
Expand All @@ -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()

1 change: 1 addition & 0 deletions app/models/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
51 changes: 51 additions & 0 deletions app/models/train.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from pydantic import BaseModel, Field
from typing import List

class PassengerProfile(BaseModel):
name: str
age: int
profession: str
personality: str
role: str
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):
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 WagonNames(BaseModel):
wagonId: str
players: List[PlayerName]

class WagonPlayerDetails(BaseModel):
wagonId: str
players: List[PlayerDetails]


class WagonsResponse(BaseModel):
wagons: List[Wagon]

class GenerateTrainResponse(BaseModel):
names: List[WagonNames]
player_details: List[WagonPlayerDetails]
wagons: List[Wagon]
13 changes: 7 additions & 6 deletions app/routes/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ class GuessResponse(BaseModel):
score: float


def get_chat_service():
return ChatService()


def get_guess_service():
return GuessingService()

Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -161,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")

Expand Down
69 changes: 69 additions & 0 deletions app/routes/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from fastapi import APIRouter, HTTPException
from app.services.generate_train.generate_train import GenerateTrainService
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(
prefix="/api/generate",
tags=["train-generation"]
)

logger = get_logger("generate")

@router.get("/train/{session_id}/{number_of_wagons}/{theme}")
async def get_generated_train(
session_id: str,
number_of_wagons: str,
theme: str
):
"""
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")

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")

try:
generate_train_service = GenerateTrainService()
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)

# 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
SessionService.update_session(session)
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)}")

Loading
Loading