Add context-aware response generator, demo session, and bug fixes
Features: - Context-aware response generator for storyteller - Select multiple characters to include in context - Generate scene descriptions or individual responses - Individual responses auto-parsed and sent to each character - Improved prompt with explicit [CharacterName] format - Smart context building with character profiles and history - Demo session auto-creation on startup - Pre-configured 'The Cursed Tavern' adventure - Two characters: Bargin (Dwarf Warrior) and Willow (Elf Ranger) - Quick-access buttons on home page - Eliminates need to recreate test data - Session ID copy button for easy sharing Bug Fixes: - Fixed character chat history showing only most recent message - CharacterView now handles both 'storyteller_response' and 'new_message' - Fixed all Pydantic deprecation warnings - Replaced .dict() with .model_dump() (9 instances) - Fixed WebSocket manager reference in contextual responses UI Improvements: - Beautiful demo section with gradient styling - Format help text for individual responses - Improved messaging and confirmations Documentation: - CONTEXTUAL_RESPONSE_FEATURE.md - Complete feature documentation - DEMO_SESSION.md - Demo session guide - FIXES_SUMMARY.md - Bug fix summary - PROMPT_IMPROVEMENTS.md - Prompt engineering details
This commit is contained in:
266
main.py
266
main.py
@@ -149,8 +149,8 @@ async def character_websocket(websocket: WebSocket, session_id: str, character_i
|
||||
character = session.characters[character_id]
|
||||
await websocket.send_json({
|
||||
"type": "history",
|
||||
"messages": [msg.dict() for msg in character.conversation_history],
|
||||
"public_messages": [msg.dict() for msg in session.public_messages]
|
||||
"messages": [msg.model_dump() for msg in character.conversation_history],
|
||||
"public_messages": [msg.model_dump() for msg in session.public_messages]
|
||||
})
|
||||
|
||||
while True:
|
||||
@@ -177,7 +177,7 @@ async def character_websocket(websocket: WebSocket, session_id: str, character_i
|
||||
await manager.send_to_client(char_key, {
|
||||
"type": "public_message",
|
||||
"character_name": character.name,
|
||||
"message": message.dict()
|
||||
"message": message.model_dump()
|
||||
})
|
||||
elif visibility == "mixed":
|
||||
session.public_messages.append(message)
|
||||
@@ -188,7 +188,7 @@ async def character_websocket(websocket: WebSocket, session_id: str, character_i
|
||||
await manager.send_to_client(char_key, {
|
||||
"type": "public_message",
|
||||
"character_name": character.name,
|
||||
"message": message.dict()
|
||||
"message": message.model_dump()
|
||||
})
|
||||
# Add to character's private conversation
|
||||
character.conversation_history.append(message)
|
||||
@@ -204,7 +204,7 @@ async def character_websocket(websocket: WebSocket, session_id: str, character_i
|
||||
"type": "character_message",
|
||||
"character_id": character_id,
|
||||
"character_name": character.name,
|
||||
"message": message.dict()
|
||||
"message": message.model_dump()
|
||||
})
|
||||
|
||||
except WebSocketDisconnect:
|
||||
@@ -231,13 +231,13 @@ async def storyteller_websocket(websocket: WebSocket, session_id: str):
|
||||
"name": char.name,
|
||||
"description": char.description,
|
||||
"personality": char.personality,
|
||||
"conversation_history": [msg.dict() for msg in char.conversation_history],
|
||||
"conversation_history": [msg.model_dump() for msg in char.conversation_history],
|
||||
"pending_response": char.pending_response
|
||||
}
|
||||
for char_id, char in session.characters.items()
|
||||
},
|
||||
"current_scene": session.current_scene,
|
||||
"public_messages": [msg.dict() for msg in session.public_messages]
|
||||
"public_messages": [msg.model_dump() for msg in session.public_messages]
|
||||
})
|
||||
|
||||
while True:
|
||||
@@ -259,7 +259,7 @@ async def storyteller_websocket(websocket: WebSocket, session_id: str):
|
||||
if char_key in manager.active_connections:
|
||||
await manager.send_to_client(char_key, {
|
||||
"type": "storyteller_response",
|
||||
"message": message.dict()
|
||||
"message": message.model_dump()
|
||||
})
|
||||
|
||||
elif data.get("type") == "narrate_scene":
|
||||
@@ -360,6 +360,177 @@ async def generate_suggestion(session_id: str, character_id: str, context: str =
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error generating suggestion: {str(e)}")
|
||||
|
||||
# Generate context-aware response with multiple characters
|
||||
class ContextualResponseRequest(BaseModel):
|
||||
character_ids: List[str] # List of character IDs to include in context
|
||||
response_type: str = "scene" # "scene" (broadcast) or "individual" (per character)
|
||||
model: str = "gpt-4o"
|
||||
additional_context: Optional[str] = None
|
||||
|
||||
@app.post("/sessions/{session_id}/generate_contextual_response")
|
||||
async def generate_contextual_response(
|
||||
session_id: str,
|
||||
request: ContextualResponseRequest
|
||||
):
|
||||
"""Generate a storyteller response using context from multiple characters"""
|
||||
if session_id not in sessions:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
session = sessions[session_id]
|
||||
|
||||
# Validate all character IDs exist
|
||||
for char_id in request.character_ids:
|
||||
if char_id not in session.characters:
|
||||
raise HTTPException(status_code=404, detail=f"Character {char_id} not found")
|
||||
|
||||
# Build context from all selected characters
|
||||
context_parts = []
|
||||
context_parts.append("You are the storyteller/game master in an RPG session. Here's what the characters have done:")
|
||||
context_parts.append("")
|
||||
|
||||
# Add current scene if available
|
||||
if session.current_scene:
|
||||
context_parts.append(f"Current Scene: {session.current_scene}")
|
||||
context_parts.append("")
|
||||
|
||||
# Add public messages for context
|
||||
if session.public_messages:
|
||||
context_parts.append("Recent public actions:")
|
||||
for msg in session.public_messages[-5:]:
|
||||
context_parts.append(f"- {msg.content}")
|
||||
context_parts.append("")
|
||||
|
||||
# Add each character's recent messages
|
||||
for char_id in request.character_ids:
|
||||
character = session.characters[char_id]
|
||||
context_parts.append(f"Character: {character.name}")
|
||||
context_parts.append(f"Description: {character.description}")
|
||||
if character.personality:
|
||||
context_parts.append(f"Personality: {character.personality}")
|
||||
|
||||
# Add recent conversation
|
||||
if character.conversation_history:
|
||||
context_parts.append("Recent messages:")
|
||||
for msg in character.conversation_history[-3:]:
|
||||
sender_label = character.name if msg.sender == "character" else "You (Storyteller)"
|
||||
context_parts.append(f" {sender_label}: {msg.content}")
|
||||
else:
|
||||
context_parts.append("(No messages yet)")
|
||||
|
||||
context_parts.append("")
|
||||
|
||||
# Add additional context if provided
|
||||
if request.additional_context:
|
||||
context_parts.append(f"Additional context: {request.additional_context}")
|
||||
context_parts.append("")
|
||||
|
||||
# Build the prompt based on response type
|
||||
if request.response_type == "scene":
|
||||
context_parts.append("Generate a scene description that addresses the actions and situations of all these characters. The scene should be vivid and incorporate what each character has done or asked about.")
|
||||
else:
|
||||
context_parts.append("Generate individual responses for each character, taking into account all their actions and the context of what's happening.")
|
||||
context_parts.append("")
|
||||
context_parts.append("IMPORTANT: Format your response EXACTLY as follows, with each character's response on a separate line:")
|
||||
context_parts.append("")
|
||||
for char_id in request.character_ids:
|
||||
char_name = session.characters[char_id].name
|
||||
context_parts.append(f"[{char_name}] Your response for {char_name} here (2-3 sentences)")
|
||||
context_parts.append("")
|
||||
context_parts.append("Use EXACTLY this format with square brackets and character names. Do not add any other text before or after.")
|
||||
|
||||
full_context = "\n".join(context_parts)
|
||||
|
||||
# Call LLM with the context
|
||||
system_prompt = "You are a creative and engaging RPG storyteller/game master."
|
||||
if request.response_type == "individual":
|
||||
system_prompt += " When asked to format responses with [CharacterName] brackets, you MUST follow that exact format precisely. Use square brackets around each character's name, followed by their response text."
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": full_context}
|
||||
]
|
||||
|
||||
try:
|
||||
response = await call_llm(request.model, messages, temperature=0.8, max_tokens=500)
|
||||
|
||||
# If individual responses, parse and send to each character
|
||||
if request.response_type == "individual":
|
||||
# Parse the response to extract individual parts
|
||||
import re
|
||||
|
||||
# Create a map of character names to IDs
|
||||
name_to_id = {session.characters[char_id].name: char_id for char_id in request.character_ids}
|
||||
|
||||
# Parse responses in format: "[CharName] response text"
|
||||
sent_responses = {}
|
||||
|
||||
for char_name, char_id in name_to_id.items():
|
||||
# Use the new square bracket format: [CharName] response text
|
||||
# This pattern captures everything after [CharName] until the next [AnotherName] or end of string
|
||||
pattern = rf'\[{re.escape(char_name)}\]\s*(.*?)(?=\[[\w\s]+\]|\Z)'
|
||||
|
||||
match = re.search(pattern, response, re.DOTALL | re.IGNORECASE)
|
||||
if match:
|
||||
individual_response = match.group(1).strip()
|
||||
|
||||
# Clean up any trailing newlines or extra whitespace
|
||||
individual_response = ' '.join(individual_response.split())
|
||||
|
||||
if individual_response: # Only send if we got actual content
|
||||
# Send to character's conversation history
|
||||
character = session.characters[char_id]
|
||||
storyteller_message = Message(
|
||||
sender="storyteller",
|
||||
content=individual_response,
|
||||
visibility="private"
|
||||
)
|
||||
character.conversation_history.append(storyteller_message)
|
||||
character.pending_response = False
|
||||
|
||||
sent_responses[char_name] = individual_response
|
||||
|
||||
# Notify via WebSocket if connected
|
||||
char_key = f"{session_id}_{char_id}"
|
||||
if char_key in manager.active_connections:
|
||||
try:
|
||||
await manager.send_to_client(char_key, {
|
||||
"type": "new_message",
|
||||
"message": storyteller_message.model_dump()
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"response": response,
|
||||
"model_used": request.model,
|
||||
"characters_included": [
|
||||
{
|
||||
"id": char_id,
|
||||
"name": session.characters[char_id].name
|
||||
}
|
||||
for char_id in request.character_ids
|
||||
],
|
||||
"response_type": request.response_type,
|
||||
"individual_responses_sent": sent_responses,
|
||||
"success": len(sent_responses) > 0
|
||||
}
|
||||
else:
|
||||
# Scene description - just return the response
|
||||
return {
|
||||
"response": response,
|
||||
"model_used": request.model,
|
||||
"characters_included": [
|
||||
{
|
||||
"id": char_id,
|
||||
"name": session.characters[char_id].name
|
||||
}
|
||||
for char_id in request.character_ids
|
||||
],
|
||||
"response_type": request.response_type
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error generating response: {str(e)}")
|
||||
|
||||
# Get available LLM models
|
||||
@app.get("/models")
|
||||
async def get_available_models():
|
||||
@@ -406,7 +577,7 @@ async def get_pending_messages(session_id: str):
|
||||
if last_message and last_message.sender == "character":
|
||||
pending[char_id] = {
|
||||
"character_name": char.name,
|
||||
"message": last_message.dict()
|
||||
"message": last_message.model_dump()
|
||||
}
|
||||
|
||||
return pending
|
||||
@@ -429,10 +600,85 @@ async def get_character_conversation(session_id: str, character_id: str):
|
||||
"description": character.description,
|
||||
"personality": character.personality
|
||||
},
|
||||
"conversation": [msg.dict() for msg in character.conversation_history],
|
||||
"conversation": [msg.model_dump() for msg in character.conversation_history],
|
||||
"pending_response": character.pending_response
|
||||
}
|
||||
|
||||
# Create a default test session on startup
|
||||
def create_demo_session():
|
||||
"""Create a pre-configured demo session for testing"""
|
||||
demo_session_id = "demo-session-001"
|
||||
|
||||
# Create session
|
||||
demo_session = GameSession(
|
||||
id=demo_session_id,
|
||||
name="The Cursed Tavern",
|
||||
current_scene="You stand outside the weathered doors of the Rusty Flagon tavern. Strange whispers echo from within, and the windows flicker with an eerie green light. The townspeople warned you about this place, but the reward for investigating is too good to pass up.",
|
||||
scene_history=["You arrive at the remote village of Millhaven at dusk, seeking adventure and fortune."]
|
||||
)
|
||||
|
||||
# Create Character 1: Bargin the Dwarf
|
||||
bargin = Character(
|
||||
id="char-bargin-001",
|
||||
name="Bargin Ironforge",
|
||||
description="A stout dwarf warrior with a braided red beard and battle-scarred armor. Carries a massive war axe named 'Grudgekeeper'.",
|
||||
personality="Brave but reckless. Loves a good fight and a strong ale. Quick to anger but fiercely loyal to companions.",
|
||||
llm_model="gpt-3.5-turbo",
|
||||
conversation_history=[],
|
||||
pending_response=False
|
||||
)
|
||||
|
||||
# Create Character 2: Willow the Elf
|
||||
willow = Character(
|
||||
id="char-willow-002",
|
||||
name="Willow Moonwhisper",
|
||||
description="An elven ranger with silver hair and piercing green eyes. Moves silently through shadows, bow always at the ready.",
|
||||
personality="Cautious and observant. Prefers to scout ahead and avoid unnecessary conflict. Has an affinity for nature and animals.",
|
||||
llm_model="gpt-3.5-turbo",
|
||||
conversation_history=[],
|
||||
pending_response=False
|
||||
)
|
||||
|
||||
# Add initial conversation for context
|
||||
initial_storyteller_msg = Message(
|
||||
sender="storyteller",
|
||||
content="Welcome to the Cursed Tavern adventure! You've been hired by the village elder to investigate strange happenings at the old tavern. Locals report seeing ghostly figures and hearing unearthly screams. Your mission: discover what's causing the disturbances and put an end to it. What would you like to do?"
|
||||
)
|
||||
|
||||
bargin.conversation_history.append(initial_storyteller_msg)
|
||||
willow.conversation_history.append(initial_storyteller_msg)
|
||||
|
||||
# Add characters to session
|
||||
demo_session.characters[bargin.id] = bargin
|
||||
demo_session.characters[willow.id] = willow
|
||||
|
||||
# Store session
|
||||
sessions[demo_session_id] = demo_session
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🎲 DEMO SESSION CREATED!")
|
||||
print(f"{'='*60}")
|
||||
print(f"Session ID: {demo_session_id}")
|
||||
print(f"Session Name: {demo_session.name}")
|
||||
print(f"\nCharacters:")
|
||||
print(f" 1. {bargin.name} (ID: {bargin.id})")
|
||||
print(f" {bargin.description}")
|
||||
print(f"\n 2. {willow.name} (ID: {willow.id})")
|
||||
print(f" {willow.description}")
|
||||
print(f"\nScenario: {demo_session.name}")
|
||||
print(f"Scene: {demo_session.current_scene[:100]}...")
|
||||
print(f"\n{'='*60}")
|
||||
print(f"To join as Storyteller: Use session ID '{demo_session_id}'")
|
||||
print(f"To join as Bargin: Use session ID '{demo_session_id}' + character ID '{bargin.id}'")
|
||||
print(f"To join as Willow: Use session ID '{demo_session_id}' + character ID '{willow.id}'")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return demo_session_id
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
# Create demo session on startup
|
||||
create_demo_session()
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
Reference in New Issue
Block a user