# This file defines "audience interaction" functions for the puppet theater system. # These functions allow external inputs (like audience actions) to modify an active TheaterSession, # such as throwing props, summoning actors, or requesting the finale. from puppet_theater.models import Actor, TheaterSession from puppet_theater.show_bible import resolve_summoned_actor_via_llm_or_default from puppet_theater.trace import add_trace_event # Maximum number of actors allowed on stage at any time MAX_ACTORS = 4 def throw_prop(session: TheaterSession | None, prop_name: str) -> TheaterSession | None: """ Simulates an audience member throwing a prop onto the stage. What it does: - Cleans up the prop name (removes extra spaces) - Adds it to the session's props list - Records it as the latest prop and audience action - Updates director logs - Emits a trace event for analytics/debugging """ if session is None: return None prop = " ".join(prop_name.strip().split()) or "mystery prop" session.props.append(prop) session.latest_prop = prop session.latest_audience_action = f"Audience threw {prop} onto the stage." session.director_log.append(session.latest_audience_action) add_trace_event( session, "audience_action", audience_action="throw_prop", prop=prop, action_summary=session.latest_audience_action, ) return session def summon_actor(session: TheaterSession | None, actor_name: str) -> TheaterSession | None: """ Allows the audience to summon a new actor onto the stage. Behavior: - Cleans the actor name - Checks if stage has space (MAX_ACTORS limit) - If full, logs a "skipped" action - Otherwise creates a new Actor and adds them to the session - Records the action in logs and trace system """ if session is None: return None name = " ".join(actor_name.strip().split()) or "Mystery Guest" # Prevent adding too many actors beyond stage limit if len(session.actors) >= MAX_ACTORS: session.latest_audience_action = "Audience tried to summon an actor, but the stage is full." session.director_log.append(session.latest_audience_action) add_trace_event( session, "audience_action", audience_action="summon_actor", validation_status="skipped_stage_full", fallback_used=False, action_summary=session.latest_audience_action, ) return session actor, summon_llm_source, summon_llm_fallback_used = resolve_summoned_actor_via_llm_or_default( session, name ) session.actors.append(actor) session.latest_audience_action = f"Audience summoned {actor.name}." session.director_log.append(session.latest_audience_action) if summon_llm_source: session.director_log.append( f"Summoned puppet profile from {summon_llm_source} (avatar {actor.avatar!s}, tools: {', '.join(actor.tools)})." ) elif summon_llm_fallback_used: session.director_log.append( "Summon LLM did not return usable JSON; used built-in guest puppet profile instead." ) add_trace_event( session, "audience_action", audience_action="summon_actor", summoned_actor=actor.name, summon_llm_source=summon_llm_source or "built_in", summon_llm_fallback_used=summon_llm_fallback_used, action_summary=session.latest_audience_action, ) return session def request_finale(session: TheaterSession | None) -> TheaterSession | None: """ Marks the session as having a requested finale. What it does: - Sets finale_requested flag to True - Updates latest audience action and director logs - Emits a trace event for analytics/debugging """ if session is None: return None session.finale_requested = True session.latest_audience_action = "Audience requested the finale." session.director_log.append(session.latest_audience_action) add_trace_event( session, "audience_action", audience_action="request_finale", action_summary=session.latest_audience_action, ) return session