Phase 2 complete

This commit is contained in:
Aodhan Collins
2025-10-12 02:18:56 +01:00
parent da30107f5b
commit 41975ecfe2
13 changed files with 3904 additions and 36 deletions

214
main.py
View File

@@ -42,11 +42,21 @@ class Message(BaseModel):
public_content: Optional[str] = None # For mixed messages - visible to all
private_content: Optional[str] = None # For mixed messages - only storyteller sees
class CharacterProfile(BaseModel):
"""Character profile with race, class, gender, and personality traits"""
gender: str = "Male" # Male, Female, Non-binary, Custom
race: str = "Human" # Human, Elf, Dwarf, Orc, Halfling
character_class: str = "Warrior" # Warrior, Wizard, Cleric, Archer, Rogue
personality_type: str = "Friendly" # Friendly, Serious, Doubtful, Measured
background: str = "" # Custom background story
avatar_data: Optional[str] = None # base64 encoded avatar image
class Character(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
description: str
personality: str = "" # Additional personality traits
personality: str = "" # Additional personality traits (legacy field)
profile: Optional[CharacterProfile] = None # Structured profile
llm_model: str = "gpt-3.5-turbo" # LLM model for this character
conversation_history: List[Message] = [] # Private conversation with storyteller
pending_response: bool = False # Waiting for storyteller response
@@ -63,6 +73,61 @@ class GameSession(BaseModel):
scene_history: List[str] = [] # All scenes narrated
public_messages: List[Message] = [] # Public messages visible to all characters
# Character Profile Prompt Templates
RACE_PROMPTS = {
"Human": "You are a human character, versatile and adaptable to any situation. You have a balanced approach to problem-solving.",
"Elf": "You are an elf, graceful and wise with centuries of experience. You have keen senses and a deep connection to nature and magic.",
"Dwarf": "You are a dwarf, stout and honorable with deep knowledge of stone and metal. You are loyal, practical, and value tradition.",
"Orc": "You are an orc, powerful and direct with a strong sense of honor and combat prowess. You value strength and straightforward action.",
"Halfling": "You are a halfling, small but brave with natural luck and a cheerful disposition. You are resourceful and enjoy the simple pleasures of life."
}
CLASS_PROMPTS = {
"Warrior": "You excel in physical combat and tactics, preferring direct action and protecting your allies. You are brave and decisive in battle.",
"Wizard": "You are a master of arcane arts, solving problems with magic and knowledge. You are intellectual, curious, and often seek understanding before action.",
"Cleric": "You channel divine power to heal and protect, guided by faith and compassion. You support your allies and seek to help those in need.",
"Archer": "You are a skilled marksman, preferring distance and precision in combat. You are patient, observant, and value accuracy over brute force.",
"Rogue": "You rely on stealth and cunning, using tricks and skills to overcome obstacles. You are clever, adaptable, and often find unconventional solutions."
}
PERSONALITY_PROMPTS = {
"Friendly": "You are friendly and approachable, always looking for the good in others. You prefer cooperation and building positive relationships.",
"Serious": "You are serious and focused, prioritizing efficiency and practical solutions. You are disciplined and value getting things done.",
"Doubtful": "You are cautious and skeptical, questioning motives and analyzing situations carefully. You prefer to be prepared for potential threats.",
"Measured": "You are measured and thoughtful, weighing options carefully before acting. You seek balance and consider multiple perspectives."
}
def build_character_system_prompt(character: Character) -> str:
"""Build system prompt from character profile"""
if not character.profile:
# Legacy character without profile
base_prompt = f"You are {character.name}. {character.description}"
if character.personality:
base_prompt += f" {character.personality}"
return base_prompt
# Build prompt from profile
profile = character.profile
race_trait = RACE_PROMPTS.get(profile.race, "")
class_trait = CLASS_PROMPTS.get(profile.character_class, "")
personality_trait = PERSONALITY_PROMPTS.get(profile.personality_type, "")
prompt_parts = [
f"You are {character.name}, a {profile.gender.lower()} {profile.race} {profile.character_class}.",
character.description,
race_trait,
class_trait,
personality_trait,
]
if profile.background:
prompt_parts.append(f"Background: {profile.background}")
if character.personality: # Legacy personality field
prompt_parts.append(character.personality)
return " ".join(filter(None, prompt_parts))
# In-memory storage (replace with database in production)
sessions: Dict[str, GameSession] = {}
@@ -98,22 +163,27 @@ async def get_session(session_id: str):
raise HTTPException(status_code=404, detail="Session not found")
return sessions[session_id]
class CreateCharacterRequest(BaseModel):
name: str
description: str
personality: str = "" # Legacy field
llm_model: str = "gpt-3.5-turbo"
profile: Optional[CharacterProfile] = None
@app.post("/sessions/{session_id}/characters/")
async def add_character(
session_id: str,
name: str,
description: str,
personality: str = "",
llm_model: str = "gpt-3.5-turbo"
request: CreateCharacterRequest
):
if session_id not in sessions:
raise HTTPException(status_code=404, detail="Session not found")
character = Character(
name=name,
description=description,
personality=personality,
llm_model=llm_model
name=request.name,
description=request.description,
personality=request.personality,
profile=request.profile,
llm_model=request.llm_model
)
session = sessions[session_id]
session.characters[character.id] = character
@@ -127,12 +197,128 @@ async def add_character(
"id": character.id,
"name": character.name,
"description": character.description,
"llm_model": character.llm_model
"llm_model": character.llm_model,
"profile": character.profile.dict() if character.profile else None
}
})
return character
# Legacy endpoint for backward compatibility
@app.post("/sessions/{session_id}/characters/legacy/")
async def add_character_legacy(
session_id: str,
name: str,
description: str,
personality: str = "",
llm_model: str = "gpt-3.5-turbo"
):
request = CreateCharacterRequest(
name=name,
description=description,
personality=personality,
llm_model=llm_model
)
return await add_character(session_id, request)
# Export character to JSON
@app.get("/sessions/{session_id}/characters/{character_id}/export")
async def export_character(session_id: str, character_id: str):
"""Export character profile to JSON"""
if session_id not in sessions:
raise HTTPException(status_code=404, detail="Session not found")
session = sessions[session_id]
if character_id not in session.characters:
raise HTTPException(status_code=404, detail="Character not found")
character = session.characters[character_id]
export_data = {
"version": "1.0",
"character": character.model_dump(),
"created_at": datetime.now().isoformat(),
"export_type": "storyteller_rpg_character"
}
return export_data
# Import character from JSON
class ImportCharacterRequest(BaseModel):
character_data: dict
@app.post("/sessions/{session_id}/characters/import")
async def import_character(session_id: str, request: ImportCharacterRequest):
"""Import character from exported JSON"""
if session_id not in sessions:
raise HTTPException(status_code=404, detail="Session not found")
try:
# Validate and extract character data
char_data = request.character_data
if "character" in char_data:
char_data = char_data["character"]
# Create character from imported data
character = Character(**char_data)
# Generate new ID to avoid conflicts
character.id = str(uuid.uuid4())
# Clear conversation history
character.conversation_history = []
character.pending_response = False
session = sessions[session_id]
session.characters[character.id] = character
# Notify storyteller
storyteller_key = f"{session_id}_storyteller"
if storyteller_key in manager.active_connections:
await manager.send_to_client(storyteller_key, {
"type": "character_joined",
"character": {
"id": character.id,
"name": character.name,
"description": character.description,
"llm_model": character.llm_model,
"profile": character.profile.dict() if character.profile else None
}
})
return character
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid character data: {str(e)}")
# Get profile options
@app.get("/profile/options")
async def get_profile_options():
"""Get available profile options for character creation"""
return {
"genders": ["Male", "Female", "Non-binary", "Custom"],
"races": list(RACE_PROMPTS.keys()),
"classes": list(CLASS_PROMPTS.keys()),
"personality_types": list(PERSONALITY_PROMPTS.keys()),
"race_descriptions": {
"Human": "Versatile and adaptable",
"Elf": "Graceful, wise, with keen senses",
"Dwarf": "Stout, loyal, master craftsmen",
"Orc": "Powerful, direct, honorable",
"Halfling": "Small, brave, lucky"
},
"class_descriptions": {
"Warrior": "Physical combat and tactics",
"Wizard": "Arcane magic and knowledge",
"Cleric": "Divine power and healing",
"Archer": "Ranged combat and precision",
"Rogue": "Stealth, cunning, and skills"
},
"personality_descriptions": {
"Friendly": "Optimistic and cooperative",
"Serious": "Focused and pragmatic",
"Doubtful": "Cautious and analytical",
"Measured": "Balanced and thoughtful"
}
}
# WebSocket endpoint for character interactions (character view)
@app.websocket("/ws/character/{session_id}/{character_id}")
async def character_websocket(websocket: WebSocket, session_id: str, character_id: str):
@@ -338,11 +524,15 @@ async def generate_suggestion(session_id: str, character_id: str, context: str =
character = session.characters[character_id]
# Prepare context for AI suggestion
# Prepare context for AI suggestion using character profile
system_prompt = build_character_system_prompt(character)
if session.current_scene:
system_prompt += f" Current scene: {session.current_scene}"
messages = [
{
"role": "system",
"content": f"You are {character.name} in an RPG. Respond in character. Character description: {character.description}. Personality: {character.personality}. Current scene: {session.current_scene}"
"content": system_prompt
}
]