Phase 2 complete
This commit is contained in:
214
main.py
214
main.py
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user