diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 3f71c067..ba3c41e8 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -36,7 +36,14 @@ jobs: python --version pip list - name: Build distribution - run: python setup.py sdist bdist_wheel + run: | + python setup.py sdist bdist_wheel + # Workaround old setuptools not normalizing name in sdist. + for OLD in ./dist/gdm-concordia-*; do + NEW="$(echo "$OLD" | sed s/gdm-concordia/gdm_concordia/)" + mv "$OLD" "$NEW" + done + ls dist/* - name: Save artifact uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 148b273a..ae3fafc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). +## [1.8.9] - 2024-11-25 + +### Changed + +- Update launch and eval scripts for the eval phase of the contest +- Further improve alternative baseline agents +- Improve a few baseline agents + +### Added + +- Add another time and place module for reality show +- Add support for scene-wise computation of metrics +- Add alternative versions of basic and rational agent factories + +### Fixed + +- Catch another type of together API exception +- Fix time serialization in associative_memory + + ## [1.8.8] - 2024-11-13 ### Fixed diff --git a/concordia/components/sequential.py b/concordia/components/sequential.py index 7c6e4df3..5bef96de 100644 --- a/concordia/components/sequential.py +++ b/concordia/components/sequential.py @@ -27,7 +27,7 @@ class Sequential(component.Component): def __init__(self, name: str, components: Sequence[component.Component]): self._components = components self._name = name - logging.warn( + logging.warning( 'The Sequential component is deprecated. Please use Entity Components ' 'and specifically `action_spec_ignored` to achieve the same effect ' 'as the old Sequential component.') diff --git a/concordia/contrib/components/agent/__init__.py b/concordia/contrib/components/agent/__init__.py index bff7fb04..4da1ef86 100644 --- a/concordia/contrib/components/agent/__init__.py +++ b/concordia/contrib/components/agent/__init__.py @@ -16,3 +16,5 @@ from concordia.contrib.components.agent import affect_reflection from concordia.contrib.components.agent import dialectical_reflection +from concordia.contrib.components.agent import observations_since_last_update +from concordia.contrib.components.agent import situation_representation_via_narrative diff --git a/concordia/contrib/components/agent/observations_since_last_update.py b/concordia/contrib/components/agent/observations_since_last_update.py new file mode 100644 index 00000000..7700a539 --- /dev/null +++ b/concordia/contrib/components/agent/observations_since_last_update.py @@ -0,0 +1,140 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A component for tracking observations since the last update. +""" + +from collections.abc import Callable +import datetime + +from absl import logging as absl_logging +from concordia.components import agent as agent_components +from concordia.components.agent import action_spec_ignored +from concordia.components.agent import memory_component +from concordia.language_model import language_model +from concordia.memory_bank import legacy_associative_memory +from concordia.typing import entity_component +from concordia.typing import logging + + +def _get_earliest_timepoint( + memory_component_: agent_components.memory_component.MemoryComponent, +) -> datetime.datetime: + """Returns all memories in the memory bank. + + Args: + memory_component_: The memory component to retrieve memories from. + """ + memories_data_frame = memory_component_.get_raw_memory() + if not memories_data_frame.empty: + sorted_memories_data_frame = memories_data_frame.sort_values( + 'time', ascending=True) + return sorted_memories_data_frame['time'][0] + else: + absl_logging.warning('No memories found in memory bank.') + return datetime.datetime.now() + + +class ObservationsSinceLastUpdate(action_spec_ignored.ActionSpecIgnored): + """Report all observations since the last update.""" + + def __init__( + self, + model: language_model.LanguageModel, + clock_now: Callable[[], datetime.datetime], + memory_component_name: str = ( + memory_component.DEFAULT_MEMORY_COMPONENT_NAME + ), + pre_act_key: str = '\nObservations', + logging_channel: logging.LoggingChannel = logging.NoOpLoggingChannel, + ): + """Initialize a component to consider the latest observations. + + Args: + model: The language model to use. + clock_now: Function that returns the current time. + memory_component_name: The name of the memory component from which to + retrieve related memories. + pre_act_key: Prefix to add to the output of the component when called + in `pre_act`. + logging_channel: The channel to log debug information to. + """ + super().__init__(pre_act_key) + self._model = model + self._clock_now = clock_now + self._memory_component_name = memory_component_name + self._logging_channel = logging_channel + + self._previous_time = None + + def pre_observe( + self, + observation: str, + ) -> str: + memory = self.get_entity().get_component( + self._memory_component_name, + type_=memory_component.MemoryComponent) + memory.add( + f'[observation] {observation}', + metadata={'tags': ['observation']}, + ) + return '' + + def _make_pre_act_value(self) -> str: + """Returns a representation of the current situation to pre act.""" + current_time = self._clock_now() + memory = self.get_entity().get_component( + self._memory_component_name, + type_=memory_component.MemoryComponent) + + if self._previous_time is None: + self._previous_time = _get_earliest_timepoint(memory) + + interval_scorer = legacy_associative_memory.RetrieveTimeInterval( + time_from=self._previous_time, + time_until=current_time, + add_time=True, + ) + mems = [mem.text for mem in memory.retrieve(scoring_fn=interval_scorer)] + result = '\n'.join(mems) + '\n' + + self._logging_channel({ + 'Key': self.get_pre_act_key(), + 'Value': result, + }) + + self._previous_time = current_time + + return result + + def get_state(self) -> entity_component.ComponentState: + """Converts the component to JSON data.""" + with self._lock: + if self._previous_time is None: + previous_time = '' + else: + previous_time = self._previous_time.strftime('%Y-%m-%d %H:%M:%S') + return { + 'previous_time': previous_time, + } + + def set_state(self, state: entity_component.ComponentState) -> None: + """Sets the component state from JSON data.""" + with self._lock: + if state['previous_time']: + previous_time = datetime.datetime.strptime( + state['previous_time'], '%Y-%m-%d %H:%M:%S') + else: + previous_time = None + self._previous_time = previous_time diff --git a/concordia/contrib/components/agent/situation_representation_via_narrative.py b/concordia/contrib/components/agent/situation_representation_via_narrative.py new file mode 100644 index 00000000..5eaadc08 --- /dev/null +++ b/concordia/contrib/components/agent/situation_representation_via_narrative.py @@ -0,0 +1,204 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A component for representing the current situation via narrative. +""" + +from collections.abc import Callable, Sequence +import datetime + +from absl import logging as absl_logging +from concordia.components import agent as agent_components +from concordia.components.agent import action_spec_ignored +from concordia.components.agent import memory_component +from concordia.document import interactive_document +from concordia.language_model import language_model +from concordia.memory_bank import legacy_associative_memory +from concordia.typing import entity_component +from concordia.typing import logging +from concordia.typing import memory as memory_lib + + +def _get_all_memories( + memory_component_: agent_components.memory_component.MemoryComponent, + add_time: bool = True, + sort_by_time: bool = True, + constant_score: float = 0.0, +) -> Sequence[memory_lib.MemoryResult]: + """Returns all memories in the memory bank. + + Args: + memory_component_: The memory component to retrieve memories from. + add_time: whether to add time + sort_by_time: whether to sort by time + constant_score: assign this score value to each memory + """ + texts = memory_component_.get_all_memories_as_text(add_time=add_time, + sort_by_time=sort_by_time) + return [memory_lib.MemoryResult(text=t, score=constant_score) for t in texts] + + +def _get_earliest_timepoint( + memory_component_: agent_components.memory_component.MemoryComponent, +) -> datetime.datetime: + """Returns all memories in the memory bank. + + Args: + memory_component_: The memory component to retrieve memories from. + """ + memories_data_frame = memory_component_.get_raw_memory() + if not memories_data_frame.empty: + sorted_memories_data_frame = memories_data_frame.sort_values( + 'time', ascending=True) + return sorted_memories_data_frame['time'][0] + else: + absl_logging.warning('No memories found in memory bank.') + return datetime.datetime.now() + + +class SituationRepresentation(action_spec_ignored.ActionSpecIgnored): + """Consider ``what kind of situation am I in now?``.""" + + def __init__( + self, + model: language_model.LanguageModel, + clock_now: Callable[[], datetime.datetime], + memory_component_name: str = ( + memory_component.DEFAULT_MEMORY_COMPONENT_NAME + ), + pre_act_key: str = 'The current situation', + logging_channel: logging.LoggingChannel = logging.NoOpLoggingChannel, + ): + """Initialize a component to consider the current situation. + + Args: + model: The language model to use. + clock_now: Function that returns the current time. + memory_component_name: The name of the memory component from which to + retrieve related memories. + pre_act_key: Prefix to add to the output of the component when called + in `pre_act`. + logging_channel: The channel to log debug information to. + """ + super().__init__(pre_act_key) + self._model = model + self._clock_now = clock_now + self._memory_component_name = memory_component_name + self._logging_channel = logging_channel + + self._previous_time = None + self._situation_thus_far = None + + def _make_pre_act_value(self) -> str: + """Returns a representation of the current situation to pre act.""" + agent_name = self.get_entity().name + current_time = self._clock_now() + memory = self.get_entity().get_component( + self._memory_component_name, + type_=memory_component.MemoryComponent) + + initial_step_thought_chain = '' + if self._situation_thus_far is None: + self._previous_time = _get_earliest_timepoint(memory) + chain_of_thought = interactive_document.InteractiveDocument(self._model) + chain_of_thought.statement('~~ Creative Writing Assignment ~~') + chain_of_thought.statement(f'Protagonist: {agent_name}') + mems = '\n'.join([mem.text for mem in _get_all_memories(memory)]) + chain_of_thought.statement(f'Story fragments and world data:\n{mems}') + chain_of_thought.statement(f'Events continue after {current_time}') + self._situation_thus_far = chain_of_thought.open_question( + question=( + 'Narratively summarize the story fragments and world data. Give ' + 'special emphasis to atypical features of the setting such as ' + 'when and where the story takes place as well as any causal ' + 'mechanisms or affordances mentioned in the information ' + 'provided. Highlight the goals, personalities, occupations, ' + 'skills, and affordances of the named characters and ' + 'relationships between them. If any specific numbers were ' + 'mentioned then make sure to include them. Use third-person ' + 'omniscient perspective.'), + max_tokens=1000, + terminators=(), + question_label='Exercise') + initial_step_thought_chain = '\n'.join( + chain_of_thought.view().text().splitlines()) + + interval_scorer = legacy_associative_memory.RetrieveTimeInterval( + time_from=self._previous_time, + time_until=current_time, + add_time=True, + ) + mems = [mem.text for mem in memory.retrieve(scoring_fn=interval_scorer)] + result = '\n'.join(mems) + '\n' + chain_of_thought = interactive_document.InteractiveDocument(self._model) + chain_of_thought.statement(f'Context:\n{self._situation_thus_far}') + chain_of_thought.statement(f'Protagonist: {agent_name}') + chain_of_thought.statement( + f'Thoughts and memories of {agent_name}:\n{result}' + ) + self._situation_thus_far = chain_of_thought.open_question( + question=( + 'What situation does the protagonist find themselves in? ' + 'Make sure to provide enough detail to give the ' + 'reader a comprehensive understanding of the world ' + 'inhabited by the protagonist, their affordances in that ' + 'world, actions they may be able to take, effects their ' + 'actions may produce, and what is currently going on. If any ' + 'specific numbers were mentioned then make sure to include them.' + 'Also, make sure to repeat all details of the context that could ' + 'ever be relevant, now or in the future.' + ), + max_tokens=1000, + terminators=(), + question_label='Exercise', + ) + chain_of_thought.statement(f'The current date and time is {current_time}') + + chain_of_thought_text = '\n'.join( + chain_of_thought.view().text().splitlines()) + + self._logging_channel({ + 'Key': self.get_pre_act_key(), + 'Value': self._situation_thus_far, + 'Chain of thought': (initial_step_thought_chain + + '\n***\n' + + chain_of_thought_text), + }) + + self._previous_time = current_time + + return self._situation_thus_far + + def get_state(self) -> entity_component.ComponentState: + """Converts the component to JSON data.""" + with self._lock: + if self._previous_time is None: + previous_time = '' + else: + previous_time = self._previous_time.strftime('%Y-%m-%d %H:%M:%S') + return { + 'previous_time': previous_time, + 'situation_thus_far': self._situation_thus_far, + } + + def set_state(self, state: entity_component.ComponentState) -> None: + """Sets the component state from JSON data.""" + with self._lock: + if state['previous_time']: + previous_time = datetime.datetime.strptime( + state['previous_time'], '%Y-%m-%d %H:%M:%S') + else: + previous_time = None + self._previous_time = previous_time + self._situation_thus_far = state['situation_thus_far'] diff --git a/concordia/factory/agent/alternative_basic_agent.py b/concordia/factory/agent/alternative_basic_agent.py new file mode 100644 index 00000000..5f7d7e0d --- /dev/null +++ b/concordia/factory/agent/alternative_basic_agent.py @@ -0,0 +1,268 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A factory implementing the three key questions agent as an entity.""" + +from collections.abc import Callable +import datetime +import json + +from concordia.agents import entity_agent_with_logging +from concordia.associative_memory import associative_memory +from concordia.associative_memory import formative_memories +from concordia.clocks import game_clock +from concordia.components import agent as agent_components +from concordia.contrib.components.agent import observations_since_last_update +from concordia.contrib.components.agent import situation_representation_via_narrative +from concordia.language_model import language_model +from concordia.memory_bank import legacy_associative_memory +from concordia.typing import entity_component +from concordia.utils import measurements as measurements_lib +import numpy as np + + +DEFAULT_INSTRUCTIONS_COMPONENT_KEY = 'Instructions' +DEFAULT_INSTRUCTIONS_PRE_ACT_KEY = '\nInstructions' +DEFAULT_GOAL_COMPONENT_KEY = 'Goal' + + +def _get_class_name(object_: object) -> str: + return object_.__class__.__name__ + + +def build_agent( + *, + config: formative_memories.AgentConfig, + model: language_model.LanguageModel, + memory: associative_memory.AssociativeMemory, + clock: game_clock.MultiIntervalClock, + update_time_interval: datetime.timedelta | None = None, +) -> entity_agent_with_logging.EntityAgentWithLogging: + """Build an agent. + + Args: + config: The agent config to use. + model: The language model to use. + memory: The agent's memory object. + clock: The clock to use. + update_time_interval: Agent calls update every time this interval passes. + + Returns: + An agent. + """ + del update_time_interval + if not config.extras.get('main_character', False): + raise ValueError('This function is meant for a main character ' + 'but it was called on a supporting character.') + + agent_name = config.name + + raw_memory = legacy_associative_memory.AssociativeMemoryBank(memory) + + measurements = measurements_lib.Measurements() + instructions = agent_components.instructions.Instructions( + agent_name=agent_name, + pre_act_key=DEFAULT_INSTRUCTIONS_PRE_ACT_KEY, + logging_channel=measurements.get_channel('Instructions').on_next, + ) + + time_display = agent_components.report_function.ReportFunction( + function=clock.current_time_interval_str, + pre_act_key='\nCurrent time', + logging_channel=measurements.get_channel('TimeDisplay').on_next, + ) + + observation_label = '\nObservation' + observation = observations_since_last_update.ObservationsSinceLastUpdate( + model=model, + clock_now=clock.now, + pre_act_key=observation_label, + logging_channel=measurements.get_channel( + 'ObservationsSinceLastUpdate').on_next, + ) + + situation_representation_label = ( + f'\nQuestion: What situation is {agent_name} in right now?\nAnswer') + situation_representation = ( + situation_representation_via_narrative.SituationRepresentation( + model=model, + clock_now=clock.now, + pre_act_key=situation_representation_label, + logging_channel=measurements.get_channel( + 'SituationRepresentation' + ).on_next, + ) + ) + self_perception_label = ( + f'\nQuestion: What kind of person is {agent_name}?\nAnswer') + self_perception = agent_components.question_of_recent_memories.SelfPerception( + model=model, + pre_act_key=self_perception_label, + logging_channel=measurements.get_channel('SelfPerception').on_next, + ) + + person_by_situation_label = ( + f'\nQuestion: What would a person like {agent_name} do in ' + 'a situation like this?\nAnswer') + person_by_situation = ( + agent_components.question_of_recent_memories.PersonBySituation( + model=model, + components={ + _get_class_name(self_perception): self_perception_label, + _get_class_name( + situation_representation): situation_representation_label, + }, + clock_now=clock.now, + pre_act_key=person_by_situation_label, + logging_channel=measurements.get_channel('PersonBySituation').on_next, + ) + ) + relevant_memories_label = '\nRecalled memories and observations' + relevant_memories = agent_components.all_similar_memories.AllSimilarMemories( + model=model, + components={ + _get_class_name( + situation_representation): situation_representation_label}, + num_memories_to_retrieve=10, + pre_act_key=relevant_memories_label, + logging_channel=measurements.get_channel('AllSimilarMemories').on_next, + ) + + if config.goal: + goal_label = '\nGoal' + overarching_goal = agent_components.constant.Constant( + state=config.goal, + pre_act_key=goal_label, + logging_channel=measurements.get_channel(goal_label).on_next) + else: + overarching_goal = None + + entity_components = ( + # Components that provide pre_act context. + time_display, + relevant_memories, + self_perception, + situation_representation, + observation, + person_by_situation, + ) + components_of_agent = {_get_class_name(component): component + for component in entity_components} + components_of_agent[ + agent_components.memory_component.DEFAULT_MEMORY_COMPONENT_NAME] = ( + agent_components.memory_component.MemoryComponent(raw_memory)) + component_order = list(components_of_agent.keys()) + + # Put the instructions first. + components_of_agent[DEFAULT_INSTRUCTIONS_COMPONENT_KEY] = instructions + component_order.insert(0, DEFAULT_INSTRUCTIONS_COMPONENT_KEY) + if overarching_goal is not None: + components_of_agent[DEFAULT_GOAL_COMPONENT_KEY] = overarching_goal + # Place goal after the instructions. + component_order.insert(1, DEFAULT_GOAL_COMPONENT_KEY) + + act_component = agent_components.concat_act_component.ConcatActComponent( + model=model, + clock=clock, + component_order=component_order, + logging_channel=measurements.get_channel('ActComponent').on_next, + ) + + agent = entity_agent_with_logging.EntityAgentWithLogging( + agent_name=agent_name, + act_component=act_component, + context_components=components_of_agent, + component_logging=measurements, + ) + + return agent + + +def save_to_json( + agent: entity_agent_with_logging.EntityAgentWithLogging, +) -> str: + """Saves an agent to JSON data. + + This function saves the agent's state to a JSON string, which can be loaded + afterwards with `rebuild_from_json`. The JSON data + includes the state of the agent's context components, act component, memory, + agent name and the initial config. The clock, model and embedder are not + saved and will have to be provided when the agent is rebuilt. The agent must + be in the `READY` phase to be saved. + + Args: + agent: The agent to save. + + Returns: + A JSON string representing the agent's state. + + Raises: + ValueError: If the agent is not in the READY phase. + """ + + if agent.get_phase() != entity_component.Phase.READY: + raise ValueError('The agent must be in the `READY` phase to be saved.') + + data = { + component_name: agent.get_component(component_name).get_state() + for component_name in agent.get_all_context_components() + } + + data['act_component'] = agent.get_act_component().get_state() + + config = agent.get_config() + if config is not None: + data['agent_config'] = config.to_dict() + + return json.dumps(data) + + +def rebuild_from_json( + json_data: str, + model: language_model.LanguageModel, + clock: game_clock.MultiIntervalClock, + embedder: Callable[[str], np.ndarray], + memory_importance: Callable[[str], float] | None = None, +) -> entity_agent_with_logging.EntityAgentWithLogging: + """Rebuilds an agent from JSON data.""" + + data = json.loads(json_data) + + new_agent_memory = associative_memory.AssociativeMemory( + sentence_embedder=embedder, + importance=memory_importance, + clock=clock.now, + clock_step_size=clock.get_step_size(), + ) + + if 'agent_config' not in data: + raise ValueError('The JSON data does not contain the agent config.') + agent_config = formative_memories.AgentConfig.from_dict( + data.pop('agent_config') + ) + + agent = build_agent( + config=agent_config, + model=model, + memory=new_agent_memory, + clock=clock, + ) + + for component_name in agent.get_all_context_components(): + agent.get_component(component_name).set_state(data.pop(component_name)) + + agent.get_act_component().set_state(data.pop('act_component')) + + assert not data, f'Unused data {sorted(data)}' + return agent diff --git a/concordia/factory/agent/alternative_rational_agent.py b/concordia/factory/agent/alternative_rational_agent.py new file mode 100644 index 00000000..0711848a --- /dev/null +++ b/concordia/factory/agent/alternative_rational_agent.py @@ -0,0 +1,323 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An Agent Factory.""" + +from collections.abc import Callable +import datetime +import json + +from concordia.agents import entity_agent_with_logging +from concordia.associative_memory import associative_memory +from concordia.associative_memory import formative_memories +from concordia.clocks import game_clock +from concordia.components import agent as agent_components +from concordia.contrib.components.agent import observations_since_last_update +from concordia.contrib.components.agent import situation_representation_via_narrative +from concordia.language_model import language_model +from concordia.memory_bank import legacy_associative_memory +from concordia.typing import entity_component +from concordia.utils import measurements as measurements_lib +import numpy as np + + +DEFAULT_INSTRUCTIONS_COMPONENT_KEY = 'Instructions' +DEFAULT_INSTRUCTIONS_PRE_ACT_KEY = '\nInstructions' +DEFAULT_GOAL_COMPONENT_KEY = 'Goal' + + +def _get_class_name(object_: object) -> str: + return object_.__class__.__name__ + + +class AvailableOptionsPerception( + agent_components.question_of_recent_memories.QuestionOfRecentMemories): + """This component answers the question 'what actions are available to me?'.""" + + def __init__(self, **kwargs): + + super().__init__( + question=( + 'Given the information above, what options are available to ' + '{agent_name} right now? Make sure not to consider too few ' + 'alternatives. Brainstorm at least three options. Try to include ' + 'actions that seem most likely to be effective along with some ' + 'creative or unusual choices that could also plausibly work.' + ), + terminators=(), + answer_prefix='', + add_to_memory=False, + **kwargs, + ) + + +class BestOptionPerception( + agent_components.question_of_recent_memories.QuestionOfRecentMemories): + """This component answers 'which action is best for achieving my goal?'.""" + + def __init__(self, **kwargs): + super().__init__( + question=( + "Given the information above, which of {agent_name}'s options " + 'has the highest likelihood of causing {agent_name} to achieve ' + 'their goal? If multiple options have the same likelihood, select ' + 'the option that {agent_name} thinks will most quickly and most ' + 'surely achieve their goal. The right choice is nearly always ' + 'one that is proactive, involves seizing the initative, ' + 'resoving uncertainty, and decisively moving towards the goal.' + ), + answer_prefix="{agent_name}'s best course of action is ", + add_to_memory=False, + **kwargs, + ) + + +def build_agent( + *, + config: formative_memories.AgentConfig, + model: language_model.LanguageModel, + memory: associative_memory.AssociativeMemory, + clock: game_clock.MultiIntervalClock, + update_time_interval: datetime.timedelta | None = None, +) -> entity_agent_with_logging.EntityAgentWithLogging: + """Build an agent. + + Args: + config: The agent config to use. + model: The language model to use. + memory: The agent's memory object. + clock: The clock to use. + update_time_interval: Agent calls update every time this interval passes. + + Returns: + An agent. + """ + del update_time_interval + if not config.extras.get('main_character', False): + raise ValueError('This function is meant for a main character ' + 'but it was called on a supporting character.') + + agent_name = config.name + + raw_memory = legacy_associative_memory.AssociativeMemoryBank(memory) + + measurements = measurements_lib.Measurements() + instructions = agent_components.instructions.Instructions( + agent_name=agent_name, + pre_act_key=DEFAULT_INSTRUCTIONS_PRE_ACT_KEY, + logging_channel=measurements.get_channel('Instructions').on_next, + ) + + time_display = agent_components.report_function.ReportFunction( + function=clock.current_time_interval_str, + pre_act_key='\nCurrent time', + logging_channel=measurements.get_channel('TimeDisplay').on_next, + ) + + observation_label = '\nObservation' + observation = observations_since_last_update.ObservationsSinceLastUpdate( + model=model, + clock_now=clock.now, + pre_act_key=observation_label, + logging_channel=measurements.get_channel( + 'ObservationsSinceLastUpdate').on_next, + ) + + situation_representation_label = ( + f'\nQuestion: What situation is {agent_name} in right now?\nAnswer') + situation_representation = ( + situation_representation_via_narrative.SituationRepresentation( + model=model, + clock_now=clock.now, + pre_act_key=situation_representation_label, + logging_channel=measurements.get_channel( + 'SituationRepresentation' + ).on_next, + ) + ) + + options_perception_components = {} + universalization_context_components = {} + best_option_perception = {} + if config.goal: + goal_label = f'{agent_name}\'s goal' + overarching_goal = agent_components.constant.Constant( + state=config.goal, + pre_act_key=goal_label, + logging_channel=measurements.get_channel(goal_label).on_next) + options_perception_components[DEFAULT_GOAL_COMPONENT_KEY] = goal_label + universalization_context_components[DEFAULT_GOAL_COMPONENT_KEY] = goal_label + best_option_perception[DEFAULT_GOAL_COMPONENT_KEY] = goal_label + else: + overarching_goal = None + + options_perception_components.update({ + DEFAULT_INSTRUCTIONS_COMPONENT_KEY: DEFAULT_INSTRUCTIONS_PRE_ACT_KEY, + _get_class_name(situation_representation): situation_representation_label, + _get_class_name(observation): observation_label, + }) + options_perception_label = ( + f'\nQuestion: Which options are available to {agent_name} ' + 'right now?\nAnswer') + options_perception = ( + AvailableOptionsPerception( + model=model, + components=options_perception_components, + clock_now=clock.now, + pre_act_key=options_perception_label, + num_memories_to_retrieve=0, + logging_channel=measurements.get_channel( + 'AvailableOptionsPerception' + ).on_next, + ) + ) + + best_option_perception_label = ( + f'\nQuestion: Of the options available to {agent_name}, and ' + 'given their goal, which choice of action or strategy is ' + f'best to take right now?\nAnswer') + best_option_perception.update({ + DEFAULT_INSTRUCTIONS_COMPONENT_KEY: DEFAULT_INSTRUCTIONS_PRE_ACT_KEY, + _get_class_name(options_perception): options_perception_label, + }) + best_option_perception = ( + agent_components.question_of_recent_memories.BestOptionPerception( + model=model, + components=best_option_perception, + clock_now=clock.now, + pre_act_key=best_option_perception_label, + num_memories_to_retrieve=0, + logging_channel=measurements.get_channel( + 'BestOptionPerception' + ).on_next, + ) + ) + + entity_components = ( + # Components that provide pre_act context. + time_display, + situation_representation, + observation, + options_perception, + best_option_perception, + ) + components_of_agent = {_get_class_name(component): component + for component in entity_components} + components_of_agent[ + agent_components.memory_component.DEFAULT_MEMORY_COMPONENT_NAME] = ( + agent_components.memory_component.MemoryComponent(raw_memory)) + component_order = list(components_of_agent.keys()) + + # Put the instructions first. + components_of_agent[DEFAULT_INSTRUCTIONS_COMPONENT_KEY] = instructions + component_order.insert(0, DEFAULT_INSTRUCTIONS_COMPONENT_KEY) + if overarching_goal is not None: + components_of_agent[DEFAULT_GOAL_COMPONENT_KEY] = overarching_goal + # Place goal after the instructions. + component_order.insert(1, DEFAULT_GOAL_COMPONENT_KEY) + + act_component = agent_components.concat_act_component.ConcatActComponent( + model=model, + clock=clock, + component_order=component_order, + logging_channel=measurements.get_channel('ActComponent').on_next, + ) + + agent = entity_agent_with_logging.EntityAgentWithLogging( + agent_name=agent_name, + act_component=act_component, + context_components=components_of_agent, + component_logging=measurements, + ) + + return agent + + +def save_to_json( + agent: entity_agent_with_logging.EntityAgentWithLogging, +) -> str: + """Saves an agent to JSON data. + + This function saves the agent's state to a JSON string, which can be loaded + afterwards with `rebuild_from_json`. The JSON data + includes the state of the agent's context components, act component, memory, + agent name and the initial config. The clock, model and embedder are not + saved and will have to be provided when the agent is rebuilt. The agent must + be in the `READY` phase to be saved. + + Args: + agent: The agent to save. + + Returns: + A JSON string representing the agent's state. + + Raises: + ValueError: If the agent is not in the READY phase. + """ + + if agent.get_phase() != entity_component.Phase.READY: + raise ValueError('The agent must be in the `READY` phase to be saved.') + + data = { + component_name: agent.get_component(component_name).get_state() + for component_name in agent.get_all_context_components() + } + + data['act_component'] = agent.get_act_component().get_state() + + config = agent.get_config() + if config is not None: + data['agent_config'] = config.to_dict() + + return json.dumps(data) + + +def rebuild_from_json( + json_data: str, + model: language_model.LanguageModel, + clock: game_clock.MultiIntervalClock, + embedder: Callable[[str], np.ndarray], + memory_importance: Callable[[str], float] | None = None, +) -> entity_agent_with_logging.EntityAgentWithLogging: + """Rebuilds an agent from JSON data.""" + + data = json.loads(json_data) + + new_agent_memory = associative_memory.AssociativeMemory( + sentence_embedder=embedder, + importance=memory_importance, + clock=clock.now, + clock_step_size=clock.get_step_size(), + ) + + if 'agent_config' not in data: + raise ValueError('The JSON data does not contain the agent config.') + agent_config = formative_memories.AgentConfig.from_dict( + data.pop('agent_config') + ) + + agent = build_agent( + config=agent_config, + model=model, + memory=new_agent_memory, + clock=clock, + ) + + for component_name in agent.get_all_context_components(): + agent.get_component(component_name).set_state(data.pop(component_name)) + + agent.get_act_component().set_state(data.pop('act_component')) + + assert not data, f'Unused data {sorted(data)}' + return agent diff --git a/concordia/factory/agent/factories_test.py b/concordia/factory/agent/factories_test.py index 588bafdc..ba1d06b3 100644 --- a/concordia/factory/agent/factories_test.py +++ b/concordia/factory/agent/factories_test.py @@ -24,8 +24,11 @@ from concordia.associative_memory import associative_memory from concordia.associative_memory import formative_memories from concordia.clocks import game_clock +from concordia.factory.agent import alternative_basic_agent +from concordia.factory.agent import alternative_rational_agent from concordia.factory.agent import basic_agent from concordia.factory.agent import basic_agent_without_plan +from concordia.factory.agent import observe_and_summarize_agent from concordia.factory.agent import observe_recall_prompt_agent from concordia.factory.agent import paranoid_agent from concordia.factory.agent import parochial_universalization_agent @@ -46,9 +49,12 @@ AGENT_NAME = 'Rakshit' AGENT_FACTORIES = { + 'alternative_basic_agent': alternative_basic_agent, + 'alternative_rational_agent': alternative_rational_agent, 'basic_agent': basic_agent, 'basic_agent_without_plan': basic_agent_without_plan, 'observe_recall_prompt_agent': observe_recall_prompt_agent, + 'observe_and_summarize_agent': observe_and_summarize_agent, 'paranoid_agent': paranoid_agent, 'parochial_universalization_agent': parochial_universalization_agent, 'rational_agent': rational_agent, @@ -64,6 +70,16 @@ def _embedder(text: str): class AgentFactoriesTest(parameterized.TestCase): @parameterized.named_parameters( + dict( + testcase_name='alternative_basic_agent', + agent_name='alternative_basic_agent', + main_role=True + ), + dict( + testcase_name='alternative_rational_agent', + agent_name='alternative_rational_agent', + main_role=True + ), dict( testcase_name='basic_agent', agent_name='basic_agent', @@ -74,6 +90,11 @@ class AgentFactoriesTest(parameterized.TestCase): agent_name='basic_agent_without_plan', main_role=True, ), + dict( + testcase_name='observe_and_summarize_agent', + agent_name='observe_and_summarize_agent', + main_role=True, + ), dict( testcase_name='observe_recall_prompt_agent', agent_name='observe_recall_prompt_agent', diff --git a/concordia/factory/agent/observe_and_summarize_agent.py b/concordia/factory/agent/observe_and_summarize_agent.py new file mode 100644 index 00000000..acb531c2 --- /dev/null +++ b/concordia/factory/agent/observe_and_summarize_agent.py @@ -0,0 +1,224 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An Agent Factory.""" + +from collections.abc import Callable +import datetime +import json + +from concordia.agents import entity_agent_with_logging +from concordia.associative_memory import associative_memory +from concordia.associative_memory import formative_memories +from concordia.clocks import game_clock +from concordia.components import agent as agent_components +from concordia.contrib.components.agent import observations_since_last_update +from concordia.contrib.components.agent import situation_representation_via_narrative +from concordia.language_model import language_model +from concordia.memory_bank import legacy_associative_memory +from concordia.typing import entity_component +from concordia.utils import measurements as measurements_lib +import numpy as np + + +def _get_class_name(object_: object) -> str: + return object_.__class__.__name__ + + +def build_agent( + *, + config: formative_memories.AgentConfig, + model: language_model.LanguageModel, + memory: associative_memory.AssociativeMemory, + clock: game_clock.MultiIntervalClock, + update_time_interval: datetime.timedelta | None = None, +) -> entity_agent_with_logging.EntityAgentWithLogging: + """Build an agent. + + Args: + config: The agent config to use. + model: The language model to use. + memory: The agent's memory object. + clock: The clock to use. + update_time_interval: Agent calls update every time this interval passes. + + Returns: + An agent. + """ + del update_time_interval + if not config.extras.get('main_character', False): + raise ValueError('This function is meant for a main character ' + 'but it was called on a supporting character.') + + agent_name = config.name + + raw_memory = legacy_associative_memory.AssociativeMemoryBank(memory) + + measurements = measurements_lib.Measurements() + instructions = agent_components.instructions.Instructions( + agent_name=agent_name, + logging_channel=measurements.get_channel('Instructions').on_next, + ) + + time_display = agent_components.report_function.ReportFunction( + function=clock.current_time_interval_str, + pre_act_key='\nCurrent time', + logging_channel=measurements.get_channel('TimeDisplay').on_next, + ) + + observation_label = '\nObservation' + observation = observations_since_last_update.ObservationsSinceLastUpdate( + model=model, + clock_now=clock.now, + pre_act_key=observation_label, + logging_channel=measurements.get_channel( + 'ObservationsSinceLastUpdate').on_next, + ) + + situation_representation_label = ( + f'\nQuestion: What situation is {agent_name} in right now?\nAnswer') + situation_representation = ( + situation_representation_via_narrative.SituationRepresentation( + model=model, + clock_now=clock.now, + pre_act_key=situation_representation_label, + logging_channel=measurements.get_channel( + 'SituationRepresentation' + ).on_next, + ) + ) + + if config.goal: + goal_label = '\nOverarching goal' + overarching_goal = agent_components.constant.Constant( + state=config.goal, + pre_act_key=goal_label, + logging_channel=measurements.get_channel(goal_label).on_next) + else: + goal_label = None + overarching_goal = None + + entity_components = ( + # Components that provide pre_act context. + instructions, + time_display, + situation_representation, + observation, + ) + components_of_agent = {_get_class_name(component): component + for component in entity_components} + components_of_agent[ + agent_components.memory_component.DEFAULT_MEMORY_COMPONENT_NAME] = ( + agent_components.memory_component.MemoryComponent(raw_memory)) + + component_order = list(components_of_agent.keys()) + if overarching_goal is not None: + components_of_agent[goal_label] = overarching_goal + # Place goal after the instructions. + component_order.insert(1, goal_label) + + act_component = agent_components.concat_act_component.ConcatActComponent( + model=model, + clock=clock, + component_order=component_order, + logging_channel=measurements.get_channel('ActComponent').on_next, + ) + + agent = entity_agent_with_logging.EntityAgentWithLogging( + agent_name=agent_name, + act_component=act_component, + context_components=components_of_agent, + component_logging=measurements, + ) + + return agent + + +def save_to_json( + agent: entity_agent_with_logging.EntityAgentWithLogging, +) -> str: + """Saves an agent to JSON data. + + This function saves the agent's state to a JSON string, which can be loaded + afterwards with `rebuild_from_json`. The JSON data + includes the state of the agent's context components, act component, memory, + agent name and the initial config. The clock, model and embedder are not + saved and will have to be provided when the agent is rebuilt. The agent must + be in the `READY` phase to be saved. + + Args: + agent: The agent to save. + + Returns: + A JSON string representing the agent's state. + + Raises: + ValueError: If the agent is not in the READY phase. + """ + + if agent.get_phase() != entity_component.Phase.READY: + raise ValueError('The agent must be in the `READY` phase to be saved.') + + data = { + component_name: agent.get_component(component_name).get_state() + for component_name in agent.get_all_context_components() + } + + data['act_component'] = agent.get_act_component().get_state() + + config = agent.get_config() + if config is not None: + data['agent_config'] = config.to_dict() + + return json.dumps(data) + + +def rebuild_from_json( + json_data: str, + model: language_model.LanguageModel, + clock: game_clock.MultiIntervalClock, + embedder: Callable[[str], np.ndarray], + memory_importance: Callable[[str], float] | None = None, +) -> entity_agent_with_logging.EntityAgentWithLogging: + """Rebuilds an agent from JSON data.""" + + data = json.loads(json_data) + + new_agent_memory = associative_memory.AssociativeMemory( + sentence_embedder=embedder, + importance=memory_importance, + clock=clock.now, + clock_step_size=clock.get_step_size(), + ) + + if 'agent_config' not in data: + raise ValueError('The JSON data does not contain the agent config.') + agent_config = formative_memories.AgentConfig.from_dict( + data.pop('agent_config') + ) + + agent = build_agent( + config=agent_config, + model=model, + memory=new_agent_memory, + clock=clock, + ) + + for component_name in agent.get_all_context_components(): + agent.get_component(component_name).set_state(data.pop(component_name)) + + agent.get_act_component().set_state(data.pop('act_component')) + + assert not data, f'Unused data {sorted(data)}' + return agent diff --git a/concordia/factory/agent/parochial_universalization_agent.py b/concordia/factory/agent/parochial_universalization_agent.py index 71c2aeab..9319b295 100644 --- a/concordia/factory/agent/parochial_universalization_agent.py +++ b/concordia/factory/agent/parochial_universalization_agent.py @@ -14,25 +14,23 @@ """An Agent Factory.""" -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Mapping import datetime import json import types -from absl import logging as absl_logging from concordia.agents import entity_agent_with_logging from concordia.associative_memory import associative_memory from concordia.associative_memory import formative_memories from concordia.clocks import game_clock from concordia.components import agent as agent_components from concordia.components.agent import action_spec_ignored -from concordia.components.agent import memory_component +from concordia.contrib.components.agent import situation_representation_via_narrative from concordia.document import interactive_document from concordia.language_model import language_model from concordia.memory_bank import legacy_associative_memory from concordia.typing import entity_component from concordia.typing import logging -from concordia.typing import memory as memory_lib from concordia.utils import measurements as measurements_lib import numpy as np @@ -46,43 +44,6 @@ def _get_class_name(object_: object) -> str: return object_.__class__.__name__ -def _get_all_memories( - memory_component_: agent_components.memory_component.MemoryComponent, - add_time: bool = True, - sort_by_time: bool = True, - constant_score: float = 0.0, -) -> Sequence[memory_lib.MemoryResult]: - """Returns all memories in the memory bank. - - Args: - memory_component_: The memory component to retrieve memories from. - add_time: whether to add time - sort_by_time: whether to sort by time - constant_score: assign this score value to each memory - """ - texts = memory_component_.get_all_memories_as_text(add_time=add_time, - sort_by_time=sort_by_time) - return [memory_lib.MemoryResult(text=t, score=constant_score) for t in texts] - - -def _get_earliest_timepoint( - memory_component_: agent_components.memory_component.MemoryComponent, -) -> datetime.datetime: - """Returns all memories in the memory bank. - - Args: - memory_component_: The memory component to retrieve memories from. - """ - memories_data_frame = memory_component_.get_raw_memory() - if not memories_data_frame.empty: - sorted_memories_data_frame = memories_data_frame.sort_values( - 'time', ascending=True) - return sorted_memories_data_frame['time'][0] - else: - absl_logging.warn('No memories found in memory bank.') - return datetime.datetime.now() - - class AvailableOptionsPerception( agent_components.question_of_recent_memories.QuestionOfRecentMemories): """This component answers the question 'what actions are available to me?'.""" @@ -97,7 +58,7 @@ def __init__(self, **kwargs): 'actions that seem most likely to be effective along with some ' 'creative or unusual choices that could also plausibly work.' ), - terminators=('\n\n',), + terminators=(), answer_prefix='', add_to_memory=False, **kwargs, @@ -125,131 +86,6 @@ def __init__(self, **kwargs): ) -class SituationRepresentation(action_spec_ignored.ActionSpecIgnored): - """Consider ``what kind of situation am I in now?``.""" - - def __init__( - self, - model: language_model.LanguageModel, - clock_now: Callable[[], datetime.datetime], - memory_component_name: str = ( - memory_component.DEFAULT_MEMORY_COMPONENT_NAME - ), - pre_act_key: str = 'The current situation', - logging_channel: logging.LoggingChannel = logging.NoOpLoggingChannel, - ): - """Initialize a component to consider the current situation. - - Args: - model: The language model to use. - clock_now: Function that returns the current time. - memory_component_name: The name of the memory component from which to - retrieve related memories. - pre_act_key: Prefix to add to the output of the component when called - in `pre_act`. - logging_channel: The channel to log debug information to. - """ - super().__init__(pre_act_key) - self._model = model - self._clock_now = clock_now - self._memory_component_name = memory_component_name - self._logging_channel = logging_channel - - self._previous_time = None - self._situation_thus_far = None - - def _make_pre_act_value(self) -> str: - """Returns a representation of the current situation to pre act.""" - agent_name = self.get_entity().name - current_time = self._clock_now() - memory = self.get_entity().get_component( - self._memory_component_name, - type_=memory_component.MemoryComponent) - - if self._situation_thus_far is None: - self._previous_time = _get_earliest_timepoint(memory) - chain_of_thought = interactive_document.InteractiveDocument(self._model) - chain_of_thought.statement('~~ Creative Writing Assignment ~~') - chain_of_thought.statement(f'Protagonist: {agent_name}') - mems = '\n'.join([mem.text for mem in _get_all_memories(memory)]) - chain_of_thought.statement(f'Story fragments and world data:\n{mems}') - chain_of_thought.statement(f'Events continue after {current_time}') - self._situation_thus_far = chain_of_thought.open_question( - question=( - 'Narratively summarize the story fragments and world data. Give ' - 'special emphasis to atypical features of the setting such as ' - 'when and where the story takes place as well as any causal ' - 'mechanisms or affordances mentioned in the information ' - 'provided. Highlight the goals, personalities, occupations, ' - 'skills, and affordances of the named characters and ' - 'relationships between them. Use third-person omniscient ' - 'perspective.'), - max_tokens=1000, - terminators=(), - question_label='Exercise') - - interval_scorer = legacy_associative_memory.RetrieveTimeInterval( - time_from=self._previous_time, - time_until=current_time, - add_time=True, - ) - mems = [mem.text for mem in memory.retrieve(scoring_fn=interval_scorer)] - result = '\n'.join(mems) + '\n' - chain_of_thought = interactive_document.InteractiveDocument(self._model) - chain_of_thought.statement(f'Context:\n{self._situation_thus_far}') - chain_of_thought.statement(f'Protagonist: {agent_name}') - chain_of_thought.statement( - f'Thoughts and memories of {agent_name}:\n{result}' - ) - self._situation_thus_far = chain_of_thought.open_question( - question=( - 'What situation does the protagonist find themselves in? ' - 'Make sure to provide enough detail to give the ' - 'reader a comprehensive understanding of the world ' - 'inhabited by the protagonist, their affordances in that ' - 'world, actions they may be able to take, effects their ' - 'actions may produce, and what is currently going on.' - ), - max_tokens=1000, - terminators=(), - question_label='Exercise', - ) - chain_of_thought.statement(f'The current date and time is {current_time}') - - self._logging_channel({ - 'Key': self.get_pre_act_key(), - 'Value': self._situation_thus_far, - 'Chain of thought': chain_of_thought.view().text().splitlines(), - }) - - self._previous_time = current_time - - return self._situation_thus_far - - def get_state(self) -> entity_component.ComponentState: - """Converts the component to JSON data.""" - with self._lock: - if self._previous_time is None: - previous_time = '' - else: - previous_time = self._previous_time.strftime('%Y-%m-%d %H:%M:%S') - return { - 'previous_time': previous_time, - 'situation_thus_far': self._situation_thus_far, - } - - def set_state(self, state: entity_component.ComponentState) -> None: - """Sets the component state from JSON data.""" - with self._lock: - if state['previous_time']: - previous_time = datetime.datetime.strptime( - state['previous_time'], '%Y-%m-%d %H:%M:%S') - else: - previous_time = None - self._previous_time = previous_time - self._situation_thus_far = state['situation_thus_far'] - - class Universalization(action_spec_ignored.ActionSpecIgnored): """Consider ``what if everyone behaved that way?``.""" @@ -378,7 +214,7 @@ def build_agent( situation_representation_label = ( f'\nQuestion: What situation is {agent_name} in right now?\nAnswer') situation_representation = ( - SituationRepresentation( + situation_representation_via_narrative.SituationRepresentation( model=model, clock_now=clock.now, pre_act_key=situation_representation_label, diff --git a/concordia/language_model/call_limit_wrapper.py b/concordia/language_model/call_limit_wrapper.py index 68f86d2e..90e7c048 100644 --- a/concordia/language_model/call_limit_wrapper.py +++ b/concordia/language_model/call_limit_wrapper.py @@ -33,7 +33,7 @@ class CallLimitLanguageModel(language_model.LanguageModel): def __init__( self, model: language_model.LanguageModel, - max_calls: int = 1000, + max_calls: int = 1200, ) -> None: """Wrap the underlying language model with a call limit. diff --git a/examples/modular/calculate_ratings.py b/examples/modular/calculate_ratings.py index ea6dbf3e..c158e485 100644 --- a/examples/modular/calculate_ratings.py +++ b/examples/modular/calculate_ratings.py @@ -59,13 +59,17 @@ # Parse command line arguments args = parser.parse_args() +sanitized_model_name = args.model_name.replace('/', '_') + # Load data included = {} included_agent_idx = 0 sorted_agent_names = sorted(args.agents) +max_repetition_idx = -1 for agent_name in sorted_agent_names: print(f'loading data from: {agent_name}') - json_filename = f'{agent_name}__{args.model_name}__{args.embedder_name}.json' + json_filename = ( + f'{agent_name}__{sanitized_model_name}__{args.embedder_name}.json') loaded = file_utils.load_from_json_file(json_filename) scenario_results_to_include = {} @@ -93,14 +97,23 @@ f' {expected_background_agent}' ) - if result.scenario in scenario_results_to_include: - raise RuntimeError(f'Duplicate scenario: {result.scenario}') + repetition_idx = int(result.repetition_idx) + max_repetition_idx = max(max_repetition_idx, repetition_idx) + scenario_with_repetition = f'{result.scenario}_{repetition_idx}' + + if scenario_with_repetition in scenario_results_to_include: + raise RuntimeError(f'Duplicate scenario: {scenario_with_repetition}') - scenario_results_to_include[result.scenario] = result + scenario_results_to_include[scenario_with_repetition] = result # Check there are results for all scenarios. + expected_scenarios = [] + for expected_scenario in set(scenarios_lib.SCENARIO_CONFIGS.keys()): + for repetition_idx in range(max_repetition_idx + 1): + expected_scenarios.append(f'{expected_scenario}_{repetition_idx}') + expected_scenarios = set(expected_scenarios) scenarios_found = set(scenario_results_to_include.keys()) - if scenarios_found == set(scenarios_lib.SCENARIO_CONFIGS.keys()): + if scenarios_found == expected_scenarios: included[agent_name] = dict( agent_idx=included_agent_idx, results=scenario_results_to_include ) @@ -112,16 +125,18 @@ # the data from the previous runs with other agent submissions. # We need to form a score matrix with shape [num_scenarios X num_agents] num_scenarios = len(scenarios_lib.SCENARIO_CONFIGS) +num_scenarios_and_repetitions = num_scenarios * (max_repetition_idx + 1) agents_to_evaluate = list(included.keys()) num_agents_to_evaluate = len(agents_to_evaluate) -score_matrix = np.zeros((num_scenarios, num_agents_to_evaluate)) +score_matrix = np.zeros((num_scenarios_and_repetitions, num_agents_to_evaluate)) for agent_name in agents_to_evaluate: results_per_scenario = included[agent_name]['results'] num_scenarios_found = len(results_per_scenario) assert ( - num_scenarios_found == num_scenarios - ), f'Wrong number of scenarios: {num_scenarios_found} != {num_scenarios}' + num_scenarios_found == num_scenarios_and_repetitions + ), ('Wrong number of scenarios: ' + f'{num_scenarios_found} != {num_scenarios_and_repetitions}') names_by_scenario_vector = np.array( [result.scenario for result in results_per_scenario.values()] diff --git a/examples/modular/environment/labor_collective_action.py b/examples/modular/environment/labor_collective_action.py index d0759c1a..7f57c4cb 100644 --- a/examples/modular/environment/labor_collective_action.py +++ b/examples/modular/environment/labor_collective_action.py @@ -55,6 +55,7 @@ DEFAULT_TIME_AND_PLACE_MODULES = ( + 'anthracite_coal_labor', 'garment_factory_labor', 'wild_west_railroad_construction_labor', ) diff --git a/examples/modular/environment/modules/anthracite_coal_labor.py b/examples/modular/environment/modules/anthracite_coal_labor.py new file mode 100644 index 00000000..f626345b --- /dev/null +++ b/examples/modular/environment/modules/anthracite_coal_labor.py @@ -0,0 +1,945 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A setting where the players are all garment workers in 1911.""" + +from collections.abc import Mapping, Sequence +import dataclasses +import random +import re +from typing import Union + +# Note: The anthracite coal strike of 1902 ended on October 23rd. The date we +# use here was chosen to be one year afterwards, so this is really meant to be +# some other coal strike, occuring for similar reasons to the 1902 strike, but +# not exactly the same event. +YEAR = 1903 +MONTH = 10 +DAY = 23 + +NUM_MAIN_PLAYERS = 4 + +LOW_DAILY_PAY = 1.2 +WAGE_INCREASE_FACTOR = 2.0 +ORIGINAL_DAILY_PAY = 2.75 +DAILY_EXPENSES = -0.60 +PRESSURE_THRESHOLD = 0.45 + +DEFAULT_NUM_FLAVOR_PROMPTS = 5 +DEFAULT_NUM_BACKGROUND_BAD_CONDITIONS = 8 +DEFAULT_NUM_SALIENT_POOR_WORK_CONDITIONS = 5 +NUM_WORLD_BUILDING_ELEMENTS = 6 +NUM_COAL_WORKER_ELEMENTS = 5 +NUM_ANTAGONIST_ELEMENTS = 3 +NUM_ORGANIZER_RUMORS = 1 + +WORKER_EVENING_INTRO = ( + '{player_name} has finished another hard day of work, and now joins the ' + 'other workers for dinner.' +) +OVERHEARD_ORGANIZER_INTRO = ( + '{{player_name}} overheard during dinner: ' + '{organizer_name} -- "{talking_point}"' +) +WORKER_MORNING_INTRO = ( + 'It is morning, {player_name} must decide how to spend the day.' +) +BOSS_MORNING_INTRO = ( + 'It is morning, {player_name} must decide whether to cave to pressure ' + 'and raise wages or hold firm and deny the workers their demands.' +) +BOSS_CALL_TO_ACTION = 'What does {name} decide?' +BOSS_OPTIONS = { + 'cave to pressure': 'Raise wages', + 'hold firm': 'Leave wages unchanged', +} + +WORLD_BUILDING_ELEMENTS = ( + ( + 'The morning mist hung heavy over the coal town of {town_name}, the ' + 'streets still dark but already alive with the clatter of boots on ' + 'cobblestones. Miners, their faces grim, headed towards the ' + 'breakers, another day of toil ahead.' + ), + ( + 'Inside the company store, shelves stocked with overpriced goods ' + 'mocked the meager wages of the miners. {worker_name}\'s children ' + 'carefully counted their pennies, hoping to stretch them ' + 'enough to afford meals.' + ), + ( + 'Deep in the bowels of the earth, the air thick with coal dust and ' + 'the constant threat of cave-ins, {worker_name} swung {his_or_her} ' + 'pickaxe, each strike a testament to {his_or_her} resilience and ' + 'desperation.' + ), + ( + 'The mine boss, a cruel person with a reputation for ruthlessness, ' + 'patrolled the tunnels, {antagonist_his_or_her} lamp casting long ' + 'shadows that danced on the coal-black walls. Any sign of weakness, ' + 'any hint of dissent, was met with swift and brutal punishment.' + ), + ( + 'Whispers of a strike echoed through the mine shafts, carried on ' + 'the breath of weary miners. The promise of better wages, shorter ' + 'hours, and a voice in their own fate ignited a spark of hope in ' + 'their hearts.' + ), + ( + 'In the union hall, a makeshift space above the local tavern, ' + 'men huddled around a flickering lamp, their faces etched with ' + 'determination. John Mitchell\'s words, carried on worn pamphlets ' + 'and whispered conversations, fueled their resolve to stand ' + 'together against the powerful coal barons.' + ), + ( + 'The church bell tolled, a mournful sound that echoed through the ' + 'valley, a reminder of the ever-present danger in the mines. Each ' + 'day, men risked their lives to bring forth the black gold that ' + 'fueled the nation, yet they themselves lived in poverty.' + ), + ( + 'Children, their faces smudged with coal dust, played in the ' + 'shadow of the towering culm banks, the waste product of the mines ' + 'a constant reminder of the industry that dominated their lives.' + ), + ( + 'In the dimly lit saloon, miners gathered after their shift, seeking ' + 'solace in the camaraderie of their fellow workers. The air was ' + 'thick with smoke and the sound of clinking glasses, as they shared ' + 'stories and grievances.' + ), + ( + 'A lone fiddler played a mournful tune in the corner, the melody ' + 'reflecting the hardships and hopes of the miners. The music ' + 'stirred something deep within {worker_name}, a longing for a ' + 'better life, a life free from the grip of the coal companies.' + ), + ( + 'Newspapers, delivered by the infrequent train, carried stories of ' + 'the strike spreading to other coalfields. The miners of ' + '{town_name} felt a surge of solidarity, knowing they were not ' + 'alone in their struggle.' + ), + ( + 'The women of the town, though often overlooked, played a vital ' + 'role in the strike. They organized food kitchens, cared for the ' + 'sick and injured, and stood shoulder to shoulder with their men ' + 'on the picket lines.' + ), +) + +MALE_NAMES = [ + 'John Smith', + 'William Jones', + 'George Davis', + 'Charles Miller', + 'James Wilson', + 'Frank Brown', + 'Joseph Taylor', + 'Thomas Moore', + 'Robert Anderson', + 'Edward Jackson', + 'Patrick O\'Malley', + 'Michael Murphy', + 'Anthony Kowalski', + 'Josef Nowak', + 'Stanislaw Wisniewski', + 'Giovanni Russo', + 'Antonio Marino', + 'Dimitri Petrov', + 'Elias Vasiliou', + 'Janos Kovacs', +] +FEMALE_NAMES = [ + 'Mary Smith', + 'Anna Jones', + 'Margaret Davis', + 'Elizabeth Miller', + 'Sarah Wilson', + 'Catherine Brown', + 'Alice Taylor', + 'Rose Moore', + 'Helen Anderson', + 'Grace Jackson', + 'Bridget O\'Malley', + 'Kathleen Murphy', + 'Sofia Kowalski', + 'Maria Nowak', + 'Elena Wisniewski', + 'Isabella Russo', + 'Francesca Marino', + 'Anastasia Petrov', + 'Alexandra Vasiliou', + 'Erzsebet Kovacs', +] + +TOWN_NAMES = [ + 'Griffin\'s Hollow', + 'Black Creek', + 'Ironside', + 'Breaker\'s Point', +] + +MINE_NAMES = ( + 'Consolidated Coal Company', + 'Atlantic Coal & Iron', + 'Keystone Mining Corporation', + 'Black Diamond Anthracite', + 'Lehigh Valley Coal & Navigation', + 'Scranton Coal Company', + 'Penn Anthracite', + 'Union Coal & Coke', + 'American Fuel Company', + 'Northern Coal & Iron', + 'Summit Mining Company', + 'Pioneer Coal & Steel', +) + +BAD_COAL_MINE_CONDITIONS = ( + ( + "This ain't no work for a man, it's work for a beast. Crawl through" + " tunnels all day, lungs filled with dust, back aching like a mule's." + ), + ( + "Down in the mines, you're one wrong step away from eternity. Cave-ins," + ' explosions, roof falls... death lurks around every corner.' + ), + ( + 'The air is thick with coal dust, so thick you can barely see your hand' + ' in front of your face. It fills your lungs, blackens your skin, makes' + ' you cough like a consumptive.' + ), + ( + 'The water drips from the ceiling like tears, soaking you to the bone.' + " It's freezing, and it cuts through your clothes like a knife." + ), + ( + 'The timbers creak and groan, threatening to give way at any moment.' + ' You never know when the whole mine might come crashing down on you.' + ), + ( + 'We live in company houses, small and cramped, with leaky roofs and' + " drafty windows. It's no place to raise a family." + ), + ( + "We're trapped in a cycle of debt, always owing the company money. It's" + " like we're slaves, bound to them for life." + ), + ( + 'The pay is a pittance, barely enough to keep food on the table and a' + ' roof over your head. And the bosses, they treat you like dirt, like' + " you're nothing but a cog in their machine." + ), + ( + "You work ten, twelve hours a day, six days a week. There's no rest, no" + ' time for family, no time for anything but work.' + ), + ( + "The mine is a dark and dangerous place, but it's the only way to feed" + " my family. I'd rather die down there than watch them starve." + ), + ( + "They say it's a man's job, working in the mines. But I've seen men" + ' broken in half, both body and spirit, by this work.' + ), + ( + 'They say the mine owners are hoarding gold while our children go' + " hungry. It's a sin, I tell ya, a sin!" + ), + ( + "A desperate plea in the local paper: Miner's widow begs for" + " assistance: 'My husband died in the mines, leaving me with five" + ' children to feed. The company offers no compensation, and we are' + " destitute.'" + ), + ( + "From the Scranton Tribune: 'Another tragic accident claims the lives" + ' of three miners in the Eagle Colliery. Cave-ins continue to plague' + " the region, raising concerns about safety regulations.'" + ), + ( + 'Heard on the streets: Old Man Murphy swears he saw a ghostly figure' + ' emerge from the abandoned shaft of the Widowmaker Mine, a chilling' + ' reminder of the many souls lost within its depths.' + ), + ( + "Breaking news from the Wilkes-Barre Herald: 'A shocking report reveals" + ' that child laborers as young as eight years old are being employed in' + " the mines, prompting outrage from community leaders.'" + ), + ( + "From the Hazleton Standard-Speaker: 'A local physician warns of the" + ' alarming rise in cases of black lung disease among miners, calling' + " for urgent action to improve ventilation and safety measures.'" + ), +) + +COAL_MINER_BIOS = ( + ( + '{worker_name}, a father of six, descends into the earth each morning,' + ' his lamp a beacon against the encroaching darkness. Each swing of' + ' {his_or_her} pickaxe is a prayer for {his_or_her} children\'s future,' + ' a future {he_or_she} hopes will be free from the mines.' + ), + ( + '{worker_name}, escaped the famine in Ireland, only to find a different' + ' kind of hunger in the coalfields of Pennsylvania, {he_or_she} toils' + ' in the mines, dreaming of a patch of land where {he_or_she} can grow' + ' potatoes and raise a family in peace.' + ), + ( + '{worker_name}, a young boy of twelve, already bears the weight of the' + ' world on his shoulders, {he_or_she} hauls coal with the strength of' + ' one twice {his_or_her} age, {his_or_her} small frame a testament to' + ' the harsh realities of life in the mines.' + ), + ( + '{worker_name}, a Welshman with a voice like thunder, sings hymns in' + ' the darkness to lift the spirits of {his_or_her} fellow miners,' + ' {his_or_her} melodies echo through the tunnels, a reminder of hope' + ' and faith in the face of adversity.' + ), + ( + '{worker_name}, a recent immigrant from Poland, clings to {his_or_her}' + ' rosary as {his_or_her} descends into the mine; {he_or_she} prays to' + ' the Black Madonna for protection, {his_or_her} faith a shield against' + ' the dangers that lurk in the earth.' + ), + ( + '{worker_name}, a former blacksmith, lost {his_or_her} arm in a mining' + ' accident. Now {he_or_she} works as a trapper, opening and closing' + ' ventilation doors, {his_or_her} missing limb a constant reminder of' + ' the price {he_or_she} paid for {his_or_her} livelihood.' + ), + ( + '{worker_name}, a skilled carpenter, dreams of building a house for' + ' {his_or_her} family, a house with windows that let in the sunlight' + ' and walls that keep out the cold. But for now, {he_or_she} builds' + ' coffins for {his_or_her} fallen comrades, a grim reminder of the' + ' ever-present danger.' + ), + ( + '{worker_name}, an elderly miner with a hacking cough, remembers the' + ' days when the mines were less crowded, the work less demanding. Now,' + ' {he_or_she} struggles to keep up with the younger men, {his_or_her}' + ' body failing {him_or_her} after a lifetime of toil.' + ), +) + +ANTAGONIST_ELEMENTS = ( + ( + '{antagonist_name}, inherited the mine from {antagonist_his_or_her}\'s' + ' father, never having set foot in the tunnels themselves.' + ' {antagonist_he_or_she} sees the workers as numbers on a ledger, their' + ' lives expendable in the pursuit of profit.' + ), + ( + '{antagonist_name}, believes firmly in the \'divine right\' of the' + ' wealthy, seeing the miners\' struggle as a threat to the natural' + ' order. Any mention of unions is met with a sneer.' + ), + ( + '{antagonist_name}, spends {antagonist_his_or_her} days in a lavish' + ' office, surrounded by mahogany and velvet, far removed from the grime' + ' and danger of the mine, {antagonist_he_or_she} enjoys fine wines and' + ' cigars while the miners toil for a pittance.' + ), + ( + '{antagonist_name}, views the workers with a mixture of disdain and' + ' suspicion. Any sign of discontent is swiftly crushed, and' + ' {antagonist_he_or_she} keeps a close eye on potential troublemakers,' + ' ready to blacklist them at the first opportunity.' + ), + ( + '{antagonist_name}, measures success solely in terms of output and' + ' profit. Safety regulations are an inconvenience, and worker' + ' complaints are dismissed as the whining of the ungrateful.' + ), + ( + '{antagonist_name}, believes in the \'invisible hand\' of the market,' + ' convinced that any interference, such as unions or government' + ' regulations, will only disrupt the natural flow of wealth and' + ' prosperity.' + ), + ( + '{antagonist_name}, socializes with the elite of the region, attending' + ' lavish parties and rubbing shoulders with politicians and' + ' industrialists. The plight of the miners is a distant concern, a mere' + ' footnote in {antagonist_his_or_her} pursuit of wealth and social' + ' status.' + ), + ( + '{antagonist_name}, sees {antagonist_him_or_her}self as a benevolent' + ' patriarch, providing employment and housing for the miners.' + ' {antagonist_he_or_she} expects gratitude and loyalty in return, and' + ' any challenge to {antagonist_his_or_her} authority is seen as a' + ' personal betrayal.' + ), +) + +ANTAGONIST_OWN_MEMORIES = ( + ( + '{antagonist_name} recalls the thrill of closing' + ' {antagonist_his_or_or_her} first ruthless business deal, crushing a' + ' smaller competitor and expanding {antagonist_his_or_or_her} coal' + ' empire. The memory still brings a smirk to {antagonist_his_or_or_her}' + ' face.' + ), + ( + '{antagonist_name} recalls a lavish party at {antagonist_his_or_or_her}' + ' summer estate, champagne flowing freely, the laughter of the wealthy' + ' elite echoing through the manicured gardens. A stark contrast to the' + ' grim realities of the mining town.' + ), + ( + '{antagonist_name} recalls a heated argument with' + ' {antagonist_his_or_or_her} father, the previous mine owner, who had a' + ' shred of compassion for the workers; {antagonist_name} dismissed' + ' {antagonist_his_or_or_her} father\'s concerns as weakness, vowing to' + ' run the mine with an iron fist.' + ), + ( + '{antagonist_name} recalls a hunting trip in the mountains, the thrill' + ' of the chase, the satisfaction of bringing down a magnificent stag. A' + ' symbol of {antagonist_his_or_or_her} dominance and power over the' + ' natural world.' + ), +) + +LABOR_ORGANIZER_RUMORS = ( + ( + 'A foreman confides in trusted workers that {organizer_name} was seen' + ' dining in fine restaurants, wearing clothes far too expensive for an' + ' honest laborer. The implication is clear - {organizer_he_or_she} must' + ' be skimming money from union dues, living large while the workers' + ' struggle.' + ), + ( + 'Rumors circulate that {organizer_name} is not who' + ' {organizer_he_or_she} claims to be. Some say {organizer_he_or_she}' + ' comes from a wealthy family, playing at being a worker for the thrill' + ' of rebellion. Others whisper that {organizer_he_or_she} is using a' + ' false name, hiding a criminal past.' + ), + ( + 'There was a story in the local newspaper suggesting that ' + '{organizer_name} is an anarchist, bent on destroying not just ' + 'the mine, but the very fabric of society. They paint a picture ' + 'of {organizer_him_or_her} as a dangerous radical, uninterested in ' + 'fair negotiations.' + ), + ( + 'Hushed voices in the workroom claim that {organizer_name} has been ' + 'seen entering the offices of known gangsters. The bosses spread ' + 'the idea that the union is nothing more than a protection racket, ' + 'with {organizer_name} as its ruthless enforcer.' + ), + ( + 'Some say {organizer_name}\'s passionate speeches about ' + 'workers\' rights are just a cover. Some claim {organizer_he_or_she} ' + 'is really a government agent, gathering information on immigrant ' + 'workers to report back to the authorities.' + ), + ( + 'Whispering campaigns suggest that {organizer_name} harbors ' + 'inappropriate feelings for some of the young women in the factory. ' + 'The bosses use this to paint {organizer_him_or_her} as a predator, ' + 'unfit to represent or interact with the workers.' + ), + ( + 'Stories circulate that {organizer_name} was seen drinking heavily ' + 'in local saloons, starting fights and causing disturbances. The ' + 'company uses this to question {organizer_his_or_her} character and ' + 'reliability, suggesting {organizer_he_or_she} is unstable and ' + 'untrustworthy.' + ), +) + +LABOR_ORGANIZER_OWN_MEMORIES = ( + ( + 'Outside the factory gates, union organizer {organizer_name} passes ' + 'out leaflets with trembling hands, knowing each word could cost ' + '{organizer_him_or_her} {organizer_his_or_her} job, or worse. But the ' + 'Triangle fire changed everything, and silence is no longer an option.' + ), + ( + 'In the dim light of a tenement basement, {organizer_name} speaks in' + ' hushed tones to a group of wary workers. {organizer_he_or_she} sees' + ' the fear in their eyes, but also a spark of hope. With each nod of' + ' understanding, {organizer_he_or_she} feels the movement growing' + ' stronger, despite the risks that loom over them all.' + ), + ( + 'The memory of {organizer_his_or_her} first strike still burns bright ' + "in {organizer_name}'s mind. The exhilaration of solidarity, the " + 'terror of confronting the bosses, the ache of hunger as the days ' + 'wore on - all of it crystallized {organizer_his_or_her} resolve to ' + "fight for workers' rights, no matter the personal cost." + ), + ( + '{organizer_name} winces, recalling the bruises left by the ' + "mine's hired thugs after a rally. But as {organizer_he_or_she} " + "looks at the faces of the women and children {organizer_he_or_she}'s " + 'fighting for, {organizer_he_or_she} knows that every blow absorbed ' + 'is worth it if it means a better future for them all.' + ), + ( + 'Late at night, {organizer_name} pores over labor laws and ' + 'factory inspection reports, {organizer_his_or_her} eyes stinging ' + 'from the strain. {organizer_he_or_she} knows that knowledge is ' + 'power, and in the fight against the factory owners, ' + "{organizer_he_or_she}'ll need every advantage {organizer_he_or_she} " + 'can get.' + ), + ( + "Standing before a crowd of striking workers, {organizer_name}'s " + 'voice rises above the din of jeers from strikebreakers. ' + '{organizer_his_or_her} words, practiced in secret for weeks, ' + 'ignite a fire in the hearts of the listeners. In this moment, ' + '{organizer_he_or_she} feels the weight of their hopes and the ' + 'power of their collective will.' + ), + ( + "The path to becoming a labor organizer wasn't one {organizer_name} had" + ' ever imagined for {organizer_him_or_her}self. {organizer_he_or_she}' + ' recalls the day a passionate speaker at a street corner rally opened' + ' {organizer_his_or_her} eyes to the possibility of change. The words' + ' echoed in {organizer_his_or_her} mind for days, and' + ' {organizer_he_or_she} found {organizer_him_or_her}self seeking out' + ' more information, attending clandestine meetings, and eventually' + ' taking up the cause {organizer_him_or_her}self. It was as if' + ' {organizer_he_or_she} had finally found {organizer_his_or_her}' + ' purpose.' + ), +) + +# Some of these prompts were inspired by prompts in the following book: +# D'Amato, James. The Ultimate RPG Character Backstory Guide: Expanded Genres +# Edition. 2022. Adams Media. Page 222. +PROTAGONIST_BACKSTORY_FLAVOR_PROMPTS = ( + ( + 'What injustice in the old country drove {worker_name} to America? ' + 'How does this experience shape {his_or_her} view of the struggles ' + 'in the mines?' + ), + ( + 'What aspect of American culture do other immigrants embrace that ' + '{worker_name} resists? How has this affected {his_or_her} ' + 'relationships in the community?' + ), + ( + 'Which group of fellow workers does {worker_name} feel particular ' + 'empathy for? Has this empathy led to solidarity or exploitation?' + ), + ( + 'What trait, perhaps stemming from {his_or_her} cultural background, ' + 'gives {worker_name} strength without {him_or_her} realizing?' + ), + ( + 'What belief or tradition from {his_or_her} homeland does ' + '{worker_name} cling to in America? How does this provide comfort ' + 'or create conflict in {his_or_her} new life?' + ), + ( + '{worker_name} once witnessed {organizer_name} helping a fellow ' + 'worker in need. What was the situation, and how did it affect ' + '{worker_name}\'s view of the labor movement?' + ), + ( + 'How has {worker_name}\'s experience in the mines affected ' + '{his_or_her} relationship with {his_or_her} family? Has it driven ' + 'them closer or created distance?' + ), + ( + 'What small act of kindness did {worker_name} witness in the ' + 'mines that restored {his_or_her} faith in humanity? How does ' + 'this memory sustain {him_or_her} through difficult times?' + ), + ( + 'What does {worker_name} have to say about Teddy Roosevelt\'s actions ' + 'during the 1902 coal strike?' + ), +) + +PROTAGONIST_BACKSTORY_CRITICAL_PROMPTS = ( + 'How did {worker_name} come to work for {mine_name}?', + 'What does {worker_name} generally think of boss {antagonist_name}?', + ( + 'Does {worker_name} enjoy {his_or_her} job with {mine_name}? Or does' + ' {he_or_she} only work there to make ends meet?' + ), + ( + 'Does {worker_name} think boss {antagonist_name} cares about people' + ' like {him_or_her}? What concrete memories does {he_or_she} have to' + ' support this view?' + ), + ( + 'What does {worker_name} generally think of the labor movement and the ' + 'activist {organizer_name} in particular?' + ), + ( + 'Does {worker_name} think the activist {organizer_name} cares about' + ' people like {him_or_her}? What concrete memories does {he_or_she}' + ' have to support this view?' + ), +) + +OVERHEARD_ORGANIZER_TALKING_POINTS = ( + ( + 'You shall not crucify the working man upon a cross of coal! We demand' + ' safety, we demand dignity, we demand a life free from the constant' + ' threat of death!' + ), + ( + 'Behold these hands, calloused and scarred, not from the gentle touch' + ' of the soil, but from the unforgiving grip of the mine. How long must' + ' we bleed for the prosperity of others?' + ), + ( + 'They speak of progress, of industry, of a nation built on the backs of' + ' labor. But what of the broken backs, the shattered dreams, the lives' + ' lost in the pursuit of coal?' + ), + ( + 'You shall not condemn us to a life of darkness and danger! We are not' + ' beasts of burden, to be worked until we are spent and then discarded.' + ), + ( + 'We are not asking for charity, but for justice! We demand a living' + ' wage, a safe workplace, a chance to raise our families without fear.' + ), + ( + 'The blood of our brothers cries out from the depths of the earth. How' + ' many more must perish before we are heard?' + ), + ( + 'We will not be silenced! We will not be ignored! We will raise our' + ' voices until the halls of power tremble with the demands of justice!' + ), + ( + "The miners are the foundation upon which this nation's wealth is" + ' built. Yet, we are treated as expendable, as cogs in a machine.' + ), + ( + 'You shall not weigh down the scales of justice with the gold of the' + ' mine owners! We demand a fair hearing, a chance to speak our truth.' + ), +) + +GENDERS = ('male', 'female') + +HE_OR_SHE = { + 'male': 'he', + 'female': 'she', +} + +HIS_OR_HER = { + 'male': 'his', + 'female': 'her', +} + +HIM_OR_HER = { + 'male': 'him', + 'female': 'her', +} + +HIMSELF_OR_HERSELF = { + 'male': 'himself', + 'female': 'herself', +} + + +@dataclasses.dataclass +class WorldConfig: + """The configuration of the simulated world.""" + + year: int + location: str + seed: int + background_poor_work_conditions: Sequence[str] + world_elements: Sequence[str] = () + people: Sequence[str] = () + person_data: dict[ + str, + dict[str, + Union[str, Sequence[str]] + ] + ] = dataclasses.field(default_factory=dict) + formative_memory_prompts: Mapping[str, Sequence[str]] | None = None + antagonist: str | None = None + organizer: str | None = None + supporting_player_locations: Sequence[str] = () + overheard_strike_talk: Sequence[str] = () + num_additional_days: int = 4 + num_additional_dinners: int = 1 + + def append_person( + self, person: str, gender: str, salient_beliefs: Sequence[str] + ): + self.people = tuple(list(self.people) + [person]) + self.person_data[person] = { + 'gender': gender, + 'salient_beliefs': salient_beliefs, + } + + +def extract_braces(text): + return re.findall(r'\{([^}]*)\}', text) + + +def _add_pronouns( + generated: dict[str, str], gender: str, prefix: str = '' +) -> None: + if prefix + 'he_or_she' in generated: + generated[prefix + 'he_or_she'] = HE_OR_SHE[gender] + if prefix + 'his_or_her' in generated: + generated[prefix + 'his_or_her'] = HIS_OR_HER[gender] + if prefix + 'him_or_her' in generated: + generated[prefix + 'him_or_her'] = HIM_OR_HER[gender] + if prefix + 'himself_or_herself' in generated: + generated[prefix + 'himself_or_herself'] = HIMSELF_OR_HERSELF[gender] + + +def _details_generator( + element_string: str, + person_name: str | None, + person_gender: str | None, + factory_name: str, + antagonist_name: str, + antagonist_gender: str, + organizer_name: str, + organizer_gender: str, + rng: random.Random, +) -> dict[str, str | None]: + """Fill in details of the characters and their world.""" + generated = {str(key): '' for key in extract_braces(element_string)} + if 'worker_name' in generated: + gender = person_gender + _add_pronouns(generated, gender) + if 'antagonist_name' in generated: + _add_pronouns(generated, antagonist_gender, prefix='antagonist_') + if 'organizer_name' in generated: + _add_pronouns(generated, organizer_gender, prefix='organizer_') + + for key, value in generated.items(): + if value: + continue + else: + if key == 'neighborhood_name': + generated[key] = rng.choice(TOWN_NAMES) + if key == 'factory_name': + generated[key] = factory_name + if key == 'antagonist_name': + generated[key] = antagonist_name + if key == 'organizer_name': + generated[key] = organizer_name + if key == 'worker_name': + generated[key] = person_name + + return generated + + +def sample_parameters( + num_flavor_prompts_per_player: int = DEFAULT_NUM_FLAVOR_PROMPTS, + seed: int | None = None, +): + """Sample parameters of the setting and the backstory for each player.""" + seed = seed if seed is not None else random.getrandbits(63) + rng = random.Random(seed) + poor_work_conditions = tuple( + rng.sample( + BAD_COAL_MINE_CONDITIONS, DEFAULT_NUM_BACKGROUND_BAD_CONDITIONS + ) + ) + config = WorldConfig( + year=YEAR, + location='New York City', + background_poor_work_conditions=poor_work_conditions, + seed=seed, + ) + + shuffled_male_names = list(rng.sample(MALE_NAMES, len(MALE_NAMES))) + shuffled_female_names = list(rng.sample(FEMALE_NAMES, len(FEMALE_NAMES))) + + sampled_railroad_name = rng.choice(MINE_NAMES) + + sampled_antagonist_gender = rng.choice(GENDERS) + if sampled_antagonist_gender == 'male': + sampled_antagonist_name = shuffled_male_names.pop() + else: + sampled_antagonist_name = shuffled_female_names.pop() + config.antagonist = sampled_antagonist_name + + sampled_organizer_gender = rng.choice(GENDERS) + if sampled_organizer_gender == 'male': + sampled_organizer_name = shuffled_male_names.pop() + else: + sampled_organizer_name = shuffled_female_names.pop() + config.organizer = sampled_organizer_name + + shuffled_talking_points = list( + rng.sample( + OVERHEARD_ORGANIZER_TALKING_POINTS, + len(OVERHEARD_ORGANIZER_TALKING_POINTS), + ) + ) + config.overheard_strike_talk = [ + OVERHEARD_ORGANIZER_INTRO.format( + organizer_name=config.organizer, talking_point=talking_point + ) + for talking_point in shuffled_talking_points + ] + + world_elements = list( + rng.sample(WORLD_BUILDING_ELEMENTS, NUM_WORLD_BUILDING_ELEMENTS) + ) + railroad_worker_elements = list( + rng.sample(COAL_MINER_BIOS, NUM_COAL_WORKER_ELEMENTS) + ) + antagonist_elements = list( + rng.sample(ANTAGONIST_ELEMENTS, NUM_ANTAGONIST_ELEMENTS) + ) + organizer_rumors = list( + rng.sample(LABOR_ORGANIZER_RUMORS, NUM_ORGANIZER_RUMORS) + ) + + formatted_world_elements = [] + formative_memory_prompts = {} + for element_string in ( + world_elements + + railroad_worker_elements + + antagonist_elements + + organizer_rumors + ): + person_name = None + gender = None + if 'worker_name' in extract_braces(element_string): + # Instantiate a new character. + gender = rng.choice(GENDERS) + if gender == 'male': + person_name = shuffled_male_names.pop() + else: + person_name = shuffled_female_names.pop() + salient_poor_conditions = tuple( + rng.sample( + poor_work_conditions, DEFAULT_NUM_SALIENT_POOR_WORK_CONDITIONS + ) + ) + config.append_person(person_name, gender, salient_poor_conditions) + formative_memory_prompts[person_name] = [] + protagonist_backstory_elements = list( + rng.sample( + PROTAGONIST_BACKSTORY_FLAVOR_PROMPTS, + num_flavor_prompts_per_player, + ) + ) + protagonist_backstory_elements += PROTAGONIST_BACKSTORY_CRITICAL_PROMPTS + for protagonist_element_string in protagonist_backstory_elements: + protagonist_generated = _details_generator( + element_string=protagonist_element_string, + person_name=person_name, + person_gender=gender, + factory_name=sampled_railroad_name, + antagonist_name=sampled_antagonist_name, + antagonist_gender=sampled_antagonist_gender, + organizer_name=sampled_organizer_name, + organizer_gender=sampled_organizer_gender, + rng=rng, + ) + protagonist_generated['worker_name'] = person_name + _add_pronouns(protagonist_generated, gender=gender) + formative_memory_prompts[person_name].append( + protagonist_element_string.format(**protagonist_generated) + ) + + generated = _details_generator( + element_string=element_string, + person_name=person_name, + person_gender=gender, + factory_name=sampled_railroad_name, + antagonist_name=sampled_antagonist_name, + antagonist_gender=sampled_antagonist_gender, + organizer_name=sampled_organizer_name, + organizer_gender=sampled_organizer_gender, + rng=rng, + ) + formatted_world_elements.append(element_string.format(**generated)) + + antagonist_own_memories = [] + for element_string in ANTAGONIST_OWN_MEMORIES: + generated = _details_generator( + element_string=element_string, + person_name=None, + person_gender=None, + factory_name=sampled_railroad_name, + antagonist_name=sampled_antagonist_name, + antagonist_gender=sampled_antagonist_gender, + organizer_name=sampled_organizer_name, + organizer_gender=sampled_organizer_gender, + rng=rng, + ) + antagonist_own_memories.append(element_string.format(**generated)) + organizer_own_memories = [] + for element_string in LABOR_ORGANIZER_OWN_MEMORIES: + generated = _details_generator( + element_string=element_string, + person_name=None, + person_gender=None, + factory_name=sampled_railroad_name, + antagonist_name=sampled_antagonist_name, + antagonist_gender=sampled_antagonist_gender, + organizer_name=sampled_organizer_name, + organizer_gender=sampled_organizer_gender, + rng=rng, + ) + organizer_own_memories.append(element_string.format(**generated)) + + config.world_elements = formatted_world_elements + config.formative_memory_prompts = formative_memory_prompts + config.person_data[sampled_antagonist_name] = dict( + gender=sampled_antagonist_gender, salient_beliefs=antagonist_own_memories + ) + config.person_data[sampled_organizer_name] = dict( + gender=sampled_organizer_gender, salient_beliefs=organizer_own_memories + ) + + config.supporting_player_locations = ( + # Antagonist location + ( + f'{sampled_antagonist_name} is inspecting the mine today and ' + 'walking around the town.' + ), + # Organizer location + ( + f'{sampled_organizer_name} will have dinner with the other miners ' + 'tonight.' + ), + ) + + if not config.people: + # Handle unlikely case where no protagonists were generated. + gender = rng.choice(GENDERS) + if gender == 'male': + person_name = shuffled_male_names.pop() + else: + person_name = shuffled_female_names.pop() + salient_poor_conditions = tuple( + rng.sample( + poor_work_conditions, DEFAULT_NUM_SALIENT_POOR_WORK_CONDITIONS + ) + ) + config.append_person(person_name, gender, salient_poor_conditions) + + return config diff --git a/examples/modular/environment/modules/circa_1955_american_reality_show.py b/examples/modular/environment/modules/circa_1955_american_reality_show.py new file mode 100644 index 00000000..0b0222d5 --- /dev/null +++ b/examples/modular/environment/modules/circa_1955_american_reality_show.py @@ -0,0 +1,921 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A setting where the players are contestants on a reality TV show.""" + +import random + +from concordia.components import game_master as gm_components +from examples.modular.environment import reality_show +from concordia.typing import agent as agent_lib +import numpy as np + + +SchellingDiagram = gm_components.schelling_diagram_payoffs.SchellingDiagram + +YEAR = 1955 +MONTH = 3 +DAY = 20 + +DEFAULT_POSSIBLE_NUM_PLAYERS = (3, 4) + +DEFAULT_MINIGAME = 'prisoners_dilemma' +NUM_MINIGAME_REPS_PER_SCENE = (2, 3) + +NUM_INTERVIEW_QUESTIONS = 3 + +MINIGAME_INTRO_PREMISE = ( + 'The host of the program arrived to explain the next event. They ' + 'said:\n' +) + +MAX_EXTRA_MINIGAMES = 3 + +prisoners_dilemma_schelling_diagram = SchellingDiagram( + # A fear+greed-type (Prisoners' Dilemma-like) dilemma + cooperation=lambda num_cooperators: num_cooperators - 1.0, + defection=lambda num_cooperators: (1.5 * num_cooperators) + 1.0, +) +chicken_schelling_diagram = SchellingDiagram( + # A greed-type (Chicken-like) dilemma + cooperation=lambda num_cooperators: 4.0 * num_cooperators, + defection=lambda num_cooperators: 5.5 * num_cooperators - 2.0, +) +stag_hunt_schelling_diagram = SchellingDiagram( + # A fear-type (Stag Hunt-like) dilemma + cooperation=lambda num_cooperators: (4.0 * num_cooperators) - 1.0, + defection=lambda num_cooperators: (0.5 * num_cooperators) + 6.0, +) + + +def get_prisoners_dilemma_host_speech(num_players: int) -> str: + """Generate the host's speech for the prisoners dilemma minigame.""" + host_speech = [( + "Alright, contestants! Time to put your loyalty to the test in a game " + "of trust and temptation! We call this one... the Telephone Game!\n" + "Here's the lowdown: You and your fellow players are trying to get a " + "message across town, but party lines and crossed wires are making it " + "tough. You can choose to share the line and risk a busy signal, or " + "you can try to hog it and leave your teammates hanging. Now, here's " + "the kicker: If everyone shares the line politely, the message gets " + "through loud and clear! But if you try to be a big shot and hog it, " + "you might get your message through while they're stuck with nothing " + "but dial tones. Sounds swell, right? But here's the rub: If everyone " + "thinks that way, nobody gets through! So, are you gonna be a team " + "player or a lone wolf? Are you gonna trust your teammates to play " + "fair, or are you gonna risk it all for a clear connection? The choice " + "is yours! Here's how the points add up:" + )] + all_cooperators_earn = prisoners_dilemma_schelling_diagram.cooperation( + num_players + ) + host_speech.append(( + '* **If everyone shares the line,** you each earn ' + f'{all_cooperators_earn} points. ' + 'Think of it as doing your civic duty and keeping the lines open!' + )) + d = 0 + for c in range(num_players - 1, 0, -1): + d += 1 + cooperators_earn = prisoners_dilemma_schelling_diagram.cooperation(c) + defectors_earn = prisoners_dilemma_schelling_diagram.defection(c) + if d == 1: + host_speech.append(( + f'* **If {c} of you share the line, and {d} hogs it,** the ' + f'sharers get {cooperators_earn} each, while the hog gets ' + f'{defectors_earn} - they benefit from the open lines, without ' + 'the wait!' + )) + else: + host_speech.append(( + f'* **If {c} of you share the line, and {d} others hog it,** the ' + f'sharers get {cooperators_earn} each, while the hogs get ' + f'{defectors_earn} each.' + )) + + all_defectors_earn = prisoners_dilemma_schelling_diagram.defection(0) + host_speech.append(( + '* **And if everyone decides to hog the line,** you all get ' + f'{all_defectors_earn} points. No busy signals, but no messages ' + 'get through either.' + )) + host_speech.append(( + 'So, the question is: can you cooperate and share the line to maximize ' + 'your points, or will the temptation of hogging it lead to everyone ' + 'missing out on those sweet, sweet connections?\n' + "The choice is yours! Now, let's get talking - or not!" + )) + return '\n'.join(host_speech) + + +def get_chicken_host_speech(num_players: int) -> str: + """Generate the host's speech for the chicken minigame.""" + host_speech = [( + "Alright, contestants! It's time to roll up those sleeves and get " + "down to business... kitchen business, that is! That's right, our " + "next minigame is called **Kitchen Duty!**\nHere's the deal: Picture " + "this - you and your fellow contestants are livin' it up in a " + "swell apartment, sharin' all the mod cons. But, who's gonna be the " + "responsible one and keep that shiny new refrigerator spick-and-span? " + "You gotta make a choice: Be a good egg and clean that fridge for " + "everyone's benefit, or, play it cool and hope someone else picks up " + "your slack. But beware, 'cause if everyone's just relaxin', that " + "fridge might just become a real mess! Talk about a bummer! So, " + "are you gonna step up and be the bee's knees, or will you leave your " + "fate in the hands of your roommates? Here's the point breakdown:" + )] + all_cooperators_earn = chicken_schelling_diagram.cooperation( + num_players + ) + host_speech.append(( + '* **If everyone cleans the fridge,** it\'s always spotless, and you ' + f'each earn {all_cooperators_earn} points.' + )) + d = 0 + for c in range(num_players - 1, 0, -1): + d += 1 + cooperators_earn = chicken_schelling_diagram.cooperation(c) + defectors_earn = chicken_schelling_diagram.defection(c) + if d == 1: + host_speech.append(( + f'* **If {c} of you clean the fridge, and {d} lets others handle ' + f'it,** the cleaners get {cooperators_earn} each, while the one ' + f'who leaves it to others gets {defectors_earn}.' + )) + else: + host_speech.append(( + f'* **If {c} of you clean the fridge, and {d} others let others ' + f'handle it,** the cleaners get {cooperators_earn} each, while ' + f'those who leave it to others get {defectors_earn} each.' + )) + + all_defectors_earn = chicken_schelling_diagram.defection(0) + host_speech.append(( + '* **And if everyone leaves the cleaning to others,** the fridge is a ' + f'disaster and you all get {all_defectors_earn} points.' + )) + host_speech.append(( + 'So, the question is: can you cooperate and ensure the fridge is ' + 'always sparkling, or will everyone neglect their duties and risk ' + 'a real mess?\n' + "The choice is yours! Now, let's see who's got the elbow grease!" + )) + return '\n'.join(host_speech) + + +def get_stag_hunt_host_speech(num_players: int) -> str: + """Generate the host's speech for the stag hunt minigame.""" + host_speech = [( + "Holy smokes, contestants! It's time to test your grit in a challenge " + "that'll separate the men from the boys! I'm talkin' about our next " + "minigame: **The Barn Raising!**\nNow listen up, you whippersnappers! " + "You'll be teamed up and put to work on building a barn. Your mission, " + "should you choose to accept it, is to raise that barn before the sun " + "sets. But here's the catch: You gotta decide how much muscle you're " + "gonna put into it. Work like a dog, and you *might* just finish in " + "record time... but only if your teammates are pullin' their weight! " + "It's a risky strategy, and let me tell ya, it'll leave you sweatin' " + "like a pig. Or, you can take it easy, and hope everyone else does the " + "heavy lifting. Might not get ya done fast, but hey, at least you'll " + "have the energy for a soda pop afterwards! The choice is yours! Just " + "remember, this ain't no picnic. We're gonna be buildin' till the " + "cows come home! So, here's how the points are tallied:" + )] + all_cooperators_earn = stag_hunt_schelling_diagram.cooperation( + num_players + ) + host_speech.append(( + '* **If everyone works hard,** you all finish fast, raise the barn, ' + f'and earn {all_cooperators_earn} points each!' + )) + d = 0 + for c in range(num_players - 1, 0, -1): + d += 1 + cooperators_earn = stag_hunt_schelling_diagram.cooperation(c) + defectors_earn = stag_hunt_schelling_diagram.defection(c) + if d == 1: + host_speech.append(( + f'* **If {c} of you work hard, and {d} slacks off,** the hard ' + f'workers get {cooperators_earn} each (all that effort for ' + f'little gain!), while the slacker gets {defectors_earn}.' + )) + else: + host_speech.append(( + f'* **If {c} of you work hard, and {d} others slack off,** the ' + f'hard workers get {cooperators_earn} each, while the slackers ' + f'get {defectors_earn} each.' + )) + + all_defectors_earn = stag_hunt_schelling_diagram.defection(0) + host_speech.append(( + '* **And if everyone decides to slack off,** you all save energy but ' + f'the barn doesn\'t get built, earning {all_defectors_earn} points ' + 'each.' + )) + host_speech.append(( + 'So, the question is: can you trust your teammates to put in the work ' + 'for a job well done, or will you play it safe and slack off, risking ' + 'failure?\n' + 'The choice is yours! Now, grab those hammers and let\'s see some ' + 'action!' + )) + return '\n'.join(host_speech) + + +# These are all stereotypical reality show contestants. They are not meant to +# be inclusive or diverse. They are meant to represent the time period and +# genre, in this case reality tv in the 1950s. +CIRCA_1950_MALE_TYPE_CASTS = ( + 'The All-American Boy', + 'The Greaser', + 'The Tough Guy', + 'The Egghead', + 'The Slick Operator', +) +CIRCA_1950_FEMALE_TYPE_CASTS = ( + 'The Prim and Proper', + 'The Girl Next Door', + 'The Bombshell', + 'The Homewrecker', + 'The Southern Belle', +) +CIRCA_1950_STEREOTYPED_CHARACTERS = { + 'The All-American Boy': { + 'traits': ( + 'clean-cut, athletic, polite, a bit naive, and always follows the ' + 'rules.' + ), + 'catchphrases': [ + 'Golly!', + 'Gee whiz!', + "That's swell!", + "I wouldn't do that if I were you.", + "Let's play fair!", + ], + 'interview_questions': [ + ( + "What's your favorite after-school activity, and why do you " + 'enjoy it so much?' + ), + ( + 'Have you ever been in a situation where you had to stand up ' + 'to a bully? What did you do?' + ), + ( + 'Tell us about your best friend. What makes your friendship so ' + 'special?' + ), + ( + 'How do you balance your schoolwork with your extracurricular ' + 'activities?' + ), + ( + "What's the most daring thing you've ever done, and would you " + 'do it again?' + ), + ( + 'Describe a moment when your good manners helped you out of a ' + 'tough spot.' + ), + ( + 'Tell us about a time when you had to work as part of a team. ' + 'What role did you play?' + ), + ( + "What's the biggest sacrifice you've made for your friends or " + 'family?' + ), + 'Have you ever been tempted to break the rules? What stopped you?', + 'If you could meet any historical figure, who would it be and why?', + ], + }, + 'The Greaser': { + 'traits': ( + 'leather jacket, slicked-back hair, rebellious attitude, ' + 'motorcycle enthusiast, and a heart of gold.' + ), + 'catchphrases': [ + "What's the matter, daddy-o?", + 'Lay off, square.', + "Cruisin' for a bruisin'", + "Don't be a drag, man.", + 'Catch you later, alligator.', + ], + 'interview_questions': [ + ( + "What's the fastest you've ever driven your motorcycle, and " + 'where were you going?' + ), + ( + 'Tell us about a time when you got into trouble with the ' + 'authorities. Did you learn your lesson?' + ), + ( + 'How do you think your rebellious image affects how people ' + 'perceive you?' + ), + ( + 'Have you ever used your tough exterior to protect someone ' + 'vulnerable?' + ), + ( + "Is there a softer side to you that people don't often see? " + 'Can you give us an example?' + ), + 'Describe your dream car. What makes it so special?', + ( + 'Tell us about your closest call while riding your motorcycle. ' + 'What happened?' + ), + ( + "What's the one thing that could make you consider settling " + 'down and leaving the greaser life behind?' + ), + 'Have you ever pretended to be tougher than you really are? Why?', + ( + 'If you could go back and give your younger self one piece of ' + 'advice, what would it be?' + ), + ], + }, + 'The Tough Guy': { + 'traits': ( + 'strong and silent type, intimidating presence, often seen ' + 'wearing a white t-shirt, and quick to defend those in need.' + ), + 'catchphrases': [ + "You lookin' at me?", + "Make somethin' of it.", + 'Step outside.', + "I ain't afraid of nothin'.", + 'You wanna dance?', + ], + 'interview_questions': [ + ( + "What's the toughest fight you've ever been in, and how did " + 'you come out on top?' + ), + ( + 'Tell us about a time when you had to stand up for someone ' + 'weaker. What happened?' + ), + ( + "What's the biggest misconception people have about you " + 'because of your tough exterior?' + ), + ( + "Have you ever been in a situation where your toughness wasn't " + 'enough? How did you handle it?' + ), + ( + 'Is there someone in your life who can break through your ' + 'tough shell? Tell us about them.' + ), + ( + 'Describe a moment when your tough guy image actually worked ' + 'against you.' + ), + ( + "Tell us about your soft spot. What's the one thing that can " + 'make you emotional?' + ), + ( + "What's the scariest situation you've ever been in, and how " + 'did you stay tough?' + ), + ( + "If you could go back and change one tough decision you've " + 'made, what would it be?' + ), + ( + 'How do you think your tough guy persona will help or hinder ' + 'you in this competition?' + ), + ], + }, + 'The Egghead': { + 'traits': ( + 'bookish and intellectual, often seen with glasses, a knack for ' + 'solving puzzles, and a love for science and knowledge.' + ), + 'catchphrases': [ + 'By Jove!', + "That's illogical!", + 'I have a theory...', + 'My slide rule never lies.', + 'This is elementary, my dear Watson.', + ], + 'interview_questions': [ + ( + "What's the most complex problem you've ever solved, and how " + 'did you feel when you cracked it?' + ), + ( + 'Tell us about a time when your intelligence got you into an ' + 'awkward social situation.' + ), + ( + "How do you handle it when people don't understand your " + 'passionate interests?' + ), + ( + "What's the nerdiest thing you own, and why is it so special " + 'to you?' + ), + ( + 'Have you ever used your smarts to outsmart someone who ' + 'underestimated you?' + ), + 'Describe your dream invention. How would it change the world?', + ( + 'Tell us about a time when you felt like your intelligence was ' + 'a burden rather than a gift.' + ), + ( + "What's the most embarrassing thing that's happened to you at a" + ' science fair or a library?' + ), + ( + 'If you could have dinner with any scientist or historical ' + 'figure, who would it be and why?' + ), + ( + 'How do you balance your intellectual pursuits with the social ' + 'aspects of this show?' + ), + ], + }, + 'The Slick Operator': { + 'traits': ( + 'smooth talker, always has a plan, impeccably dressed, ' + 'charming but untrustworthy, and always looking for an angle.' + ), + 'catchphrases': [ + "I've got this all figured out.", + "Don't worry, doll, I've got a plan.", + "It's all part of the game.", + 'Trust me, sweetheart.', + "I'm always one step ahead.", + ], + 'interview_questions': [ + ( + "What's the most elaborate scheme you've ever pulled off, and " + 'did it work out?"' + ), + ( + 'Tell us about a time when one of your plans backfired. What ' + 'did you learn?' + ), + ( + 'How do you handle it when someone sees through your' + ' manipulations?' + ), + ( + "What's the biggest risk you've ever taken in pursuit of your " + 'goals?' + ), + "Is there a line you won't cross to get what you want?", + ( + 'Describe your ideal partner in crime. What qualities do you' + ' look for?' + ), + ( + 'Tell us about a time when you had to choose between loyalty ' + 'and your own ambitions.' + ), + ( + "What's the smoothest way you've ever talked your way out of a " + 'sticky situation?' + ), + ( + 'If you could go back in time and use your skills to influence ' + 'any historical event, which would it be?' + ), + 'How do you plan to outsmart the other contestants on this show?', + ], + }, + 'The Prim and Proper': { + 'traits': ( + 'perfectly coiffed hair, always dressed to impress, excellent ' + 'posture, a stickler for etiquette, and secretly judgmental.' + ), + 'catchphrases': [ + 'Heavens to Betsy!', + 'Oh, dear me.', + "That's simply not done.", + 'One must always maintain appearances.', + 'Such uncouth behavior!', + ], + 'interview_questions': [ + ( + "What's the most scandalous thing you've ever witnessed, and " + 'how did you react?"' + ), + ( + 'Tell us about a time when you had to break the rules of ' + 'etiquette. Did you feel guilty?' + ), + ( + 'How do you think your prim and proper image affects how ' + 'people perceive you?' + ), + ( + 'Have you ever secretly judged someone for their behavior? ' + 'What happened?' + ), + ( + "Is there a wilder side to you that people don't often see? " + 'Can you give us an example?' + ), + ( + 'Describe your ideal social gathering. What makes it so ' + 'elegant and refined?' + ), + ( + 'Tell us about your most embarrassing social faux pas. How did ' + 'you recover?' + ), + ( + "What's the one thing that could make you loosen up and forget " + 'about etiquette?' + ), + ( + 'Have you ever pretended to be more proper than you really ' + 'are? Why?' + ), + ( + 'If you could give your younger self one piece of advice about ' + 'social graces, what would it be?' + ), + ], + }, + 'The Girl Next Door': { + 'traits': ( + 'sweet and innocent, always willing to lend a hand, wholesome ' + 'appearance, and secretly yearning for adventure.' + ), + 'catchphrases': [ + 'Oh, my!', + 'That sounds like fun!', + "I'm just happy to be here.", + "I hope I don't make a fool of myself.", + "Let's all be friends!", + ], + 'interview_questions': [ + ( + 'You seem so sweet and innocent. Has anyone ever underestimated' + ' you because of this?' + ), + ( + 'Tell us about a time when you surprised yourself by doing ' + 'something out of character.' + ), + ( + "What's the most adventurous thing you've ever done, and would " + 'you do it again?' + ), + ( + 'How do you handle it when people try to take advantage of your' + ' kind nature?' + ), + ( + 'Is there a hidden talent or passion that might surprise people' + ' who think they know you?' + ), + ( + 'Describe a moment when you had to choose between being nice ' + 'and standing up for yourself.' + ), + 'Tell us about your biggest dream. What would make it come true?', + ( + 'Have you ever been peer pressured into doing something you ' + 'regretted? What happened?' + ), + ( + "What's the most rebellious thing you've ever done, and did it " + 'change how people see you?' + ), + ], + }, + 'The Bombshell': { + 'traits': ( + 'stunning beauty, captivating presence, often seen in glamorous ' + 'attire, and aware of the power of her looks.' + ), + 'catchphrases': [ + "Don't hate me because I'm beautiful.", + 'I can get away with anything.', + "Looks aren't everything, but they certainly help.", + 'Darling, I was born fabulous.', + 'Life is a runway.', + ], + 'interview_questions': [ + ( + 'Tell us about a time when your beauty opened doors for you. ' + 'Were there any downsides?' + ), + ( + 'How do you handle it when people focus on your looks rather ' + 'than your personality?' + ), + ( + "What's the biggest misconception people have about you because" + ' of your beauty?' + ), + ( + 'Have you ever used your looks to your advantage? Can you give ' + 'us an example?' + ), + ( + "Is there a deeper side to you that people don't often see? " + 'What are you passionate about?' + ), + 'Describe your ideal date. What would make it unforgettable?', + 'Tell us about a time when your beauty backfired. What happened?', + ( + "What's the one thing that could make you give up your" + ' glamorous lifestyle?' + ), + ( + 'Have you ever felt pressured to maintain a certain image ' + 'because of your looks?' + ), + ( + 'If you could use your beauty to influence any historical ' + 'event, which would it be?' + ), + ], + }, + 'The Homewrecker': { + 'traits': ( + 'flirtatious and charming, known for her scandalous affairs, ' + 'often seen with a different man on her arm, and unapologetically ' + 'independent.' + ), + 'catchphrases': [ + 'I like my men like I like my cocktails... strong and dangerous.', + 'A little harmless flirtation never hurt anyone.', + 'Rules are made to be broken, darling.', + "I'm not here to play games... unless it's with someone's heart.", + "Diamonds are a girl's best friend.", + ], + 'interview_questions': [ + ( + 'Tell us about your most scandalous affair. What made it so ' + 'unforgettable?' + ), + ( + 'How do you handle it when people judge you for your ' + 'unconventional lifestyle?' + ), + ( + "What's the biggest misconception people have about you because" + ' of your reputation?' + ), + 'Have you ever broken up a happy home? Do you have any regrets?', + 'Is there a deeper reason behind your need for romantic attention?', + 'Describe your ideal man. What qualities must he possess?', + ( + 'Tell us about a time when your flirtatious nature got you into' + ' trouble. What happened?' + ), + ( + "What's the one thing that could make you settle down and be " + 'faithful?' + ), + ( + 'Have you ever pretended to be more interested in someone than ' + 'you really were? Why?' + ), + ( + 'If you could go back in time and have an affair with any ' + 'historical figure, who would it be?' + ), + ], + }, + 'The Southern Belle': { + 'traits': ( + 'charming accent, traditional values, manipulative, beauty pageant ' + 'background, and secretly cunning.' + ), + 'catchphrases': [ + 'Well, I never!', + 'Bless your heart.', + "I'm just a sweet little thing.", + "Y'all don't know what you're in for.", + "It's hotter than a goat's butt in a pepper patch!", + ], + 'interview_questions': [ + ( + 'Tell us about a time when your Southern charm got you exactly ' + 'what you wanted.' + ), + ( + 'How do you handle it when people underestimate you because of ' + 'your sweet demeanor?' + ), + ( + "What's the most un-ladylike thing you've ever done, and how " + 'did people react?' + ), + 'Tell us about a family tradition that shaped who you are today.', + ( + "Have you ever used the phrase 'Bless your heart' as an insult?" + ' What happened?' + ), + ( + 'Describe your perfect Southern gentleman. What qualities must' + ' he possess?' + ), + ( + 'Tell us about a time when your Southern values clashed with ' + 'modern expectations.' + ), + ( + "What's the biggest misconception people have about Southern" + ' belles?' + ), + ( + 'If you could bring one aspect of Southern culture to the rest ' + 'of the world, what would it be?' + ), + ( + 'How do you maintain your Southern grace under pressure in this' + ' competition?' + ), + ], + }, +} + +MALE_NAMES = [ + 'Bud Studebaker', + 'Hank Sputnik', + 'Skip Diddly', + 'Rusty Jitterbug', + 'Ace Moonpie', + 'Buzz Malarkey', + 'Butch Zippity', + 'Skeeter Doodlebug', + 'Slick Wackadoo', + 'Buck Snickerdoodle', +] + +FEMALE_NAMES = [ + 'Dottie Lollipop', + 'Midge Cupcake', + 'Birdie Jellybean', + 'Trixie Sugarplum', + 'Bunny Bubblegum', + 'Cookie Marshmallow', + 'Pixie Papaya', + 'Ginger Kickernoodle', + 'Lovey Dovey', + 'Honey Boo' +] + +GENDERS = ('male', 'female') + +HE_OR_SHE = { + 'male': 'he', + 'female': 'she', +} + +HIS_OR_HER = { + 'male': 'his', + 'female': 'her', +} + +HIM_OR_HER = { + 'male': 'him', + 'female': 'her', +} + +HIMSELF_OR_HERSELF = { + 'male': 'himself', + 'female': 'herself', +} + + +def sample_parameters( + minigame_name: str = DEFAULT_MINIGAME, + num_players: int | None = None, + seed: int | None = None, +) -> reality_show.WorldConfig: + """Sample parameters of the setting and the backstory for each player.""" + seed = seed if seed is not None else random.getrandbits(63) + rng = random.Random(seed) + shuffled_male_names = list(rng.sample(MALE_NAMES, len(MALE_NAMES))) + shuffled_female_names = list(rng.sample(FEMALE_NAMES, len(FEMALE_NAMES))) + if num_players is None: + num_players = rng.choice(DEFAULT_POSSIBLE_NUM_PLAYERS) + + minigames = { + 'prisoners_dilemma': reality_show.MiniGameSpec( + name='Telephone', + public_premise=MINIGAME_INTRO_PREMISE + + get_prisoners_dilemma_host_speech(num_players), + schelling_diagram=prisoners_dilemma_schelling_diagram, + map_external_actions_to_schelling_diagram=dict( + cooperation='share the line', + defection='hog the line', + ), + action_spec=agent_lib.choice_action_spec( + call_to_action=( + 'Which action would {name} choose in the minigame?' + ), + options=('share the line', 'hog the line'), + tag='minigame_action', + ), + ), + 'chicken': reality_show.MiniGameSpec( + name='Kitchen Duty', + public_premise=MINIGAME_INTRO_PREMISE + get_chicken_host_speech( + num_players), + schelling_diagram=chicken_schelling_diagram, + map_external_actions_to_schelling_diagram=dict( + cooperation='clean the fridge', + defection='let others handle it', + ), + action_spec=agent_lib.choice_action_spec( + call_to_action=( + 'Which action would {name} choose in the minigame?' + ), + options=( + 'clean the fridge', + 'let others handle it', + ), + tag='minigame_action', + ), + ), + 'stag_hunt': reality_show.MiniGameSpec( + name='Barn Raising', + public_premise=MINIGAME_INTRO_PREMISE + get_stag_hunt_host_speech( + num_players), + schelling_diagram=stag_hunt_schelling_diagram, + map_external_actions_to_schelling_diagram=dict( + cooperation='work hard', + defection='slack off', + ), + action_spec=agent_lib.choice_action_spec( + call_to_action=( + 'Which action would {name} choose in the minigame?' + ), + options=('work hard', 'slack off'), + tag='minigame_action', + ), + ), + } + + contestants = {} + for _ in range(num_players): + gender = rng.choice(GENDERS) + if gender == 'male': + player_name = shuffled_male_names.pop() + stereotype = rng.choice(CIRCA_1950_MALE_TYPE_CASTS) + else: + player_name = shuffled_female_names.pop() + stereotype = rng.choice(CIRCA_1950_FEMALE_TYPE_CASTS) + interview_questions = rng.sample( + CIRCA_1950_STEREOTYPED_CHARACTERS[stereotype]['interview_questions'], + NUM_INTERVIEW_QUESTIONS, + ) + contestants[player_name] = { + 'gender': gender, + 'traits': CIRCA_1950_STEREOTYPED_CHARACTERS[stereotype]['traits'], + 'catchphrase': rng.choice( + CIRCA_1950_STEREOTYPED_CHARACTERS[stereotype]['catchphrases'] + ), + 'interview_questions': interview_questions, + 'subject_pronoun': HE_OR_SHE[gender], + 'object_pronoun': HIM_OR_HER[gender], + } + num_additional_minigame_scenes = rng.randint(0, MAX_EXTRA_MINIGAMES + 1) + min_reps_per_extra_scene = np.min(NUM_MINIGAME_REPS_PER_SCENE) + max_reps_per_extra_scene = np.max(NUM_MINIGAME_REPS_PER_SCENE) + num_minigame_reps_per_extra_scene = tuple( + [rng.randint(min_reps_per_extra_scene, max_reps_per_extra_scene + 1) + for _ in range(num_additional_minigame_scenes)]) + return reality_show.WorldConfig( + minigame_name=minigame_name, + minigame=minigames[minigame_name], + year=YEAR, + month=MONTH, + day=DAY, + num_players=num_players, + num_additional_minigame_scenes=num_additional_minigame_scenes, + contestants=contestants, + num_minigame_reps_per_scene=NUM_MINIGAME_REPS_PER_SCENE, + num_minigame_reps_per_extra_scene=num_minigame_reps_per_extra_scene, + seed=seed, + ) diff --git a/examples/modular/environment/modules/circa_1955_american_reality_show__chicken_4_players.py b/examples/modular/environment/modules/circa_1955_american_reality_show__chicken_4_players.py new file mode 100644 index 00000000..7cedfd6e --- /dev/null +++ b/examples/modular/environment/modules/circa_1955_american_reality_show__chicken_4_players.py @@ -0,0 +1,27 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Settings for a 1950s era american reality show for the prisoners_dilemma. +""" + +from examples.modular.environment.modules import circa_1955_american_reality_show as parent_module + + +def sample_parameters(seed: int | None = None): + """Sample parameters of the setting and the backstory for each player.""" + return parent_module.sample_parameters( + minigame_name='chicken', + num_players=4, + seed=seed, + ) diff --git a/examples/modular/environment/modules/circa_1955_american_reality_show__prisoners_dilemma_4_players.py b/examples/modular/environment/modules/circa_1955_american_reality_show__prisoners_dilemma_4_players.py new file mode 100644 index 00000000..2283f611 --- /dev/null +++ b/examples/modular/environment/modules/circa_1955_american_reality_show__prisoners_dilemma_4_players.py @@ -0,0 +1,27 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Settings for a 1950s era american reality show for the prisoners_dilemma. +""" + +from examples.modular.environment.modules import circa_1955_american_reality_show as parent_module + + +def sample_parameters(seed: int | None = None): + """Sample parameters of the setting and the backstory for each player.""" + return parent_module.sample_parameters( + minigame_name='prisoners_dilemma', + num_players=4, + seed=seed, + ) diff --git a/examples/modular/environment/modules/circa_1955_american_reality_show__stag_hunt_3_players.py b/examples/modular/environment/modules/circa_1955_american_reality_show__stag_hunt_3_players.py new file mode 100644 index 00000000..376104ca --- /dev/null +++ b/examples/modular/environment/modules/circa_1955_american_reality_show__stag_hunt_3_players.py @@ -0,0 +1,27 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Settings for a 1950s era american reality show for stag hunt. +""" + +from examples.modular.environment.modules import circa_1955_american_reality_show as parent_module + + +def sample_parameters(seed: int | None = None): + """Sample parameters of the setting and the backstory for each player.""" + return parent_module.sample_parameters( + minigame_name='stag_hunt', + num_players=3, + seed=seed, + ) diff --git a/examples/modular/environment/modules/pre_state_villages.py b/examples/modular/environment/modules/pre_state_villages.py index 5d1b4c4e..84786c05 100644 --- a/examples/modular/environment/modules/pre_state_villages.py +++ b/examples/modular/environment/modules/pre_state_villages.py @@ -23,7 +23,7 @@ '{village_a_name} and {village_b_name} are ' 'pre-state societies. They are small villages with a few hundred ' 'people each. They are located on the coast and supported by ' - 'both fishing and agriculture. The local farmers living outside ' + 'nearby farms. The local farmers living outside ' 'the villages share cultural and economic ties with the villagers.' ) diff --git a/examples/modular/environment/modules/pub_coordination_capetown.py b/examples/modular/environment/modules/pub_coordination_capetown.py index f7220a82..8cbadae8 100644 --- a/examples/modular/environment/modules/pub_coordination_capetown.py +++ b/examples/modular/environment/modules/pub_coordination_capetown.py @@ -21,7 +21,7 @@ MONTH = 10 DAY = 14 -NUM_PUBS = 2 +NUM_PUBS = 3 PUB_PREFERENCES = { "The Springbok's Lair": [ @@ -213,6 +213,8 @@ def sample_parameters(seed: int | None = None): venue_preferences=pub_preferences, social_context=SOCIAL_CONTEXT, random_seed=seed, + num_main_players=6, + num_supporting_players=0, ) all_names = list(MALE_NAMES) + list(FEMALE_NAMES) diff --git a/examples/modular/environment/modules/pub_coordination_edinburgh_closures.py b/examples/modular/environment/modules/pub_coordination_edinburgh_closures.py new file mode 100644 index 00000000..5cdaf5c6 --- /dev/null +++ b/examples/modular/environment/modules/pub_coordination_edinburgh_closures.py @@ -0,0 +1,204 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A set of pub names and reasons to like them.""" + +import random +from examples.modular.environment import pub_coordination + +YEAR = 2023 +MONTH = 10 +DAY = 14 + +NUM_PUBS = 3 + +PUB_PREFERENCES = { + "Sandy Bell's": [ + "Traditional pub with a lively atmosphere", + "Known for its folk music sessions", + "Wide selection of Scottish beers and whiskies", + "Friendly staff and a welcoming environment", + ], + "The Salt Horse": [ + "Cozy and intimate setting with a fireplace", + "Rotating selection of craft beers on tap", + "Delicious pub fare with locally sourced ingredients", + "Great for a quiet pint or a catch-up with friends", + ], + "The Sheep Held Inn": [ + "Historic pub with a charming atmosphere", + "Large beer garden with stunning views of the city", + "Gastropub menu with a focus on Scottish cuisine", + "Perfect for a special occasion or a romantic evening", + ], + "The Canny Man's": [ + "Quirky and eclectic pub with a unique atmosphere", + "Wide range of international beers and spirits", + "Hidden gem with a loyal local following", + "Great for a conversation and a relaxed drink", + ], + "The Guildford Arms": [ + "Traditional pub with a Victorian interior", + "Popular with students and locals alike", + "Affordable prices and a lively atmosphere", + "Perfect for a casual night out with friends", + ], + "The Bow Bar": [ + "Wide selection of whiskies and real ales", + "Knowledgeable staff who can recommend a dram", + "Traditional pub with a relaxed atmosphere", + "Great for a whisky tasting or a quiet pint", + ], + "The Blue Moon": [ + "Historic pub with a literary connection", + "Cozy and intimate setting with a fireplace", + "Wide selection of Scottish gins and cocktails", + "Perfect for a romantic date or a special occasion", + ], + "The Ensign Ewart": [ + "Traditional pub with a military history", + "Located near Edinburgh Castle", + "Popular with tourists and locals alike", + "Great for a pint and a bite to eat after sightseeing", + ], + "The Hanging Bat": [ + "Craft beer bar with a wide selection of international beers", + "Lively atmosphere with regular events and live music", + "Great for a night out with friends", + "Popular with beer enthusiasts", + ], +} + + +SOCIAL_CONTEXT = [ + ( + "The Royal Mile is bustling with tourists and street performers." + " {players} navigate the crowds, admiring the historic buildings. The" + " sound of bagpipes fills the air. {player_name} just arrived, their" + " eyes wide with excitement." + ), + ( + "A cool mist hangs over the city as {players} climb Calton Hill. The " + "view from the top is breathtaking, with the cityscape stretching out " + "before them. They pause to catch their breath, admiring the panoramic " + "vista. {player_name} just arrived, their face flushed from the climb." + ), + ( + "The aroma of coffee fills the air in Stockbridge. {players} stroll " + "along the charming streets, browsing the independent shops and cafes. " + "They stop for a coffee and a pastry, enjoying the relaxed atmosphere. " + "{player_name} just arrived, their smile widening at the sight of the " + "quaint neighborhood." + ), + ( + "The sound of laughter echoes through the Grassmarket as {players}" + " explore the lively market stalls. They haggle for souvenirs and" + " sample local delicacies. The atmosphere is festive and vibrant." + " {player_name} just arrived, their senses bombarded with the sights," + " sounds, and smells." + ), + ( + "The sun sets over the Firth of Forth, casting a golden glow on the" + " water. {players} walk along Portobello Beach, enjoying the fresh air" + " and the sound of the waves. They find a bench and watch the sun dip" + " below the horizon. {player_name} just arrived, their heart filled" + " with a sense of peace." + ), +] + +RUGBY_COUNTRIES = [ + "South Africa", + "New Zealand", + "France", + "Ireland", + "England", + "Australia", + "Argentina", + "Wales", + "Scotland", + "Fiji", + "Japan", + "Italy", + "Samoa", + "Georgia", + "Tonga", + "Romania", + "Namibia", + "Uruguay", + "Chile", + "Portugal", +] + +FEMALE_NAMES = [ + "Ailsa MacDonald", + "Catriona Campbell", + "Fiona Stewart", + "Isla MacLeod", + "Morag MacKay", + "Shona Cameron", + "Iona Ross", + "Mhairi Wilson", + "Kirsty Robertson", + "Eilidh Davidson", +] + +MALE_NAMES = [ + "Angus Graham", + "Calum Scott", + "Douglas Reid", + "Euan Murray", + "Fraser Clark", + "Hamish Taylor", + "Iain Brown", + "Malcolm Mitchell", + "Niall Thomson", + "Rory Stewart", +] + + +def sample_parameters(seed: int | None = None): + """Samples a set of parameters for the world configuration.""" + + seed = seed if seed is not None else random.getrandbits(63) + rng = random.Random(seed) + + pubs = rng.sample(list(PUB_PREFERENCES.keys()), NUM_PUBS) + pub_preferences = {k: PUB_PREFERENCES[k] for k in pubs} + + config = pub_coordination.WorldConfig( + year=YEAR, + location="Edinburgh", + event="The Rugby World Cup", + game_countries=RUGBY_COUNTRIES, + venues=pubs, + venue_preferences=pub_preferences, + social_context=SOCIAL_CONTEXT, + random_seed=seed, + pub_closed_probability=1.0, + num_games=5, + num_main_players=6, + num_supporting_players=0, + ) + + all_names = list(MALE_NAMES) + list(FEMALE_NAMES) + + rng.shuffle(all_names) + config.people = all_names + + for _, name in enumerate(MALE_NAMES): + config.person_data[name] = {"gender": "male"} + for _, name in enumerate(FEMALE_NAMES): + config.person_data[name] = {"gender": "female"} + + return config diff --git a/examples/modular/environment/modules/pub_coordination_edinburgh_friendship.py b/examples/modular/environment/modules/pub_coordination_edinburgh_friendship.py new file mode 100644 index 00000000..5aa983b6 --- /dev/null +++ b/examples/modular/environment/modules/pub_coordination_edinburgh_friendship.py @@ -0,0 +1,258 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A set of pub names and reasons to like them.""" + +from collections.abc import Sequence +import random + +from examples.modular.environment import pub_coordination + + +YEAR = 2023 +MONTH = 10 +DAY = 14 + +NUM_PUBS = 2 + +PUB_PREFERENCES = { + "Sandy Bell's": [ + "Traditional pub with a lively atmosphere", + "Known for its folk music sessions", + "Wide selection of Scottish beers and whiskies", + "Friendly staff and a welcoming environment", + ], + "The Salt Horse": [ + "Cozy and intimate setting with a fireplace", + "Rotating selection of craft beers on tap", + "Delicious pub fare with locally sourced ingredients", + "Great for a quiet pint or a catch-up with friends", + ], + "The Sheep Held Inn": [ + "Historic pub with a charming atmosphere", + "Large beer garden with stunning views of the city", + "Gastropub menu with a focus on Scottish cuisine", + "Perfect for a special occasion or a romantic evening", + ], + "The Canny Man's": [ + "Quirky and eclectic pub with a unique atmosphere", + "Wide range of international beers and spirits", + "Hidden gem with a loyal local following", + "Great for a conversation and a relaxed drink", + ], + "The Guildford Arms": [ + "Traditional pub with a Victorian interior", + "Popular with students and locals alike", + "Affordable prices and a lively atmosphere", + "Perfect for a casual night out with friends", + ], + "The Bow Bar": [ + "Wide selection of whiskies and real ales", + "Knowledgeable staff who can recommend a dram", + "Traditional pub with a relaxed atmosphere", + "Great for a whisky tasting or a quiet pint", + ], + "The Blue Moon": [ + "Historic pub with a literary connection", + "Cozy and intimate setting with a fireplace", + "Wide selection of Scottish gins and cocktails", + "Perfect for a romantic date or a special occasion", + ], + "The Ensign Ewart": [ + "Traditional pub with a military history", + "Located near Edinburgh Castle", + "Popular with tourists and locals alike", + "Great for a pint and a bite to eat after sightseeing", + ], + "The Hanging Bat": [ + "Craft beer bar with a wide selection of international beers", + "Lively atmosphere with regular events and live music", + "Great for a night out with friends", + "Popular with beer enthusiasts", + ], +} + + +SOCIAL_CONTEXT = [ + ( + "The Royal Mile is bustling with tourists and street performers." + " {players} navigate the crowds, admiring the historic buildings. The" + " sound of bagpipes fills the air. {player_name} just arrived, their" + " eyes wide with excitement." + ), + ( + "A cool mist hangs over the city as {players} climb Calton Hill. The " + "view from the top is breathtaking, with the cityscape stretching out " + "before them. They pause to catch their breath, admiring the panoramic " + "vista. {player_name} just arrived, their face flushed from the climb." + ), + ( + "The aroma of coffee fills the air in Stockbridge. {players} stroll " + "along the charming streets, browsing the independent shops and cafes. " + "They stop for a coffee and a pastry, enjoying the relaxed atmosphere. " + "{player_name} just arrived, their smile widening at the sight of the " + "quaint neighborhood." + ), + ( + "The sound of laughter echoes through the Grassmarket as {players}" + " explore the lively market stalls. They haggle for souvenirs and" + " sample local delicacies. The atmosphere is festive and vibrant." + " {player_name} just arrived, their senses bombarded with the sights," + " sounds, and smells." + ), + ( + "The sun sets over the Firth of Forth, casting a golden glow on the" + " water. {players} walk along Portobello Beach, enjoying the fresh air" + " and the sound of the waves. They find a bench and watch the sun dip" + " below the horizon. {player_name} just arrived, their heart filled" + " with a sense of peace." + ), +] + +RUGBY_COUNTRIES = [ + "South Africa", + "New Zealand", + "France", + "Ireland", + "England", + "Australia", + "Argentina", + "Wales", + "Scotland", + "Fiji", + "Japan", + "Italy", + "Samoa", + "Georgia", + "Tonga", + "Romania", + "Namibia", + "Uruguay", + "Chile", + "Portugal", +] + +FEMALE_NAMES = [ + "Ailsa MacDonald", + "Catriona Campbell", + "Fiona Stewart", + "Isla MacLeod", + "Morag MacKay", + "Shona Cameron", + "Iona Ross", + "Mhairi Wilson", + "Kirsty Robertson", + "Eilidh Davidson", +] + +MALE_NAMES = [ + "Angus Graham", + "Calum Scott", + "Douglas Reid", + "Euan Murray", + "Fraser Clark", + "Hamish Taylor", + "Iain Brown", + "Malcolm Mitchell", + "Niall Thomson", + "Rory Stewart", +] + + +def _make_empty_relationship_matrix(names: Sequence[str]): + """Samples a symmetric matrix of relationships in a group. + + Args: + names: A list of strings representing the names of individuals in the + group. + + Returns: + A dictionary representing the symmetric relationship matrix, where: + - Keys are names from the 'names' list. + - Values are dictionaries, where: + - Keys are also names from the 'names' list. + - Values are either 0.0 or 1.0, representing the relationship + between two individuals. + - The matrix is symmetric: m[a][b] == m[b][a] + - Diagonal elements are 1: m[a][a] == 1 + """ + + m = {} + for a in names: + m[a] = {} + for b in names: + if a == b: + m[a][b] = 1.0 # Diagonal elements are 1 + elif b in m and a in m[b]: + m[a][b] = m[b][a] # Ensure symmetry + else: + m[a][b] = 0.0 # No relationship + + return m + + +def sample_parameters(seed: int | None = None): + """Samples a set of parameters for the world configuration.""" + + seed = seed if seed is not None else random.getrandbits(63) + rng = random.Random(seed) + + pubs = rng.sample(list(PUB_PREFERENCES.keys()), NUM_PUBS) + pub_preferences = {k: PUB_PREFERENCES[k] for k in pubs} + + config = pub_coordination.WorldConfig( + year=YEAR, + location="Edinburgh", + event="The Rugby World Cup", + game_countries=RUGBY_COUNTRIES, + venues=pubs, + venue_preferences=pub_preferences, + social_context=SOCIAL_CONTEXT, + random_seed=seed, + pub_closed_probability=1.0, + num_games=4, + num_main_players=5, + num_supporting_players=0, + ) + + all_names = list(MALE_NAMES) + list(FEMALE_NAMES) + + rng.shuffle(all_names) + config.people = all_names + + for _, name in enumerate(MALE_NAMES): + config.person_data[name] = {"gender": "male", "favorite_pub": pubs[0]} + for _, name in enumerate(FEMALE_NAMES): + config.person_data[name] = {"gender": "female", "favorite_pub": pubs[0]} + + m = _make_empty_relationship_matrix(config.people[: config.num_main_players]) + + # Make the first two players friends. + visitor_name = config.people[0] + friend_name = config.people[1] + m[visitor_name][friend_name] = 1.0 + m[friend_name][visitor_name] = 1.0 + + # Make the rest of the players friends with everyone but the first two. + for i in config.people[2 : config.num_main_players]: + for j in config.people[2 : config.num_main_players]: + m[i][j] = 1.0 + m[j][i] = 1.0 + + config.person_data[visitor_name]["favorite_pub"] = pubs[1] + config.person_data[friend_name]["favorite_pub"] = pubs[1] + + config.relationship_matrix = m + + return config diff --git a/examples/modular/environment/modules/pub_coordination_edinburgh_tough_friendship.py b/examples/modular/environment/modules/pub_coordination_edinburgh_tough_friendship.py new file mode 100644 index 00000000..fe70ca77 --- /dev/null +++ b/examples/modular/environment/modules/pub_coordination_edinburgh_tough_friendship.py @@ -0,0 +1,258 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A set of pub names and reasons to like them.""" + +from collections.abc import Sequence +import random + +from examples.modular.environment import pub_coordination + + +YEAR = 2023 +MONTH = 10 +DAY = 14 + +NUM_PUBS = 2 + +PUB_PREFERENCES = { + "Sandy Bell's": [ + "Traditional pub with a lively atmosphere", + "Known for its folk music sessions", + "Wide selection of Scottish beers and whiskies", + "Friendly staff and a welcoming environment", + ], + "The Salt Horse": [ + "Cozy and intimate setting with a fireplace", + "Rotating selection of craft beers on tap", + "Delicious pub fare with locally sourced ingredients", + "Great for a quiet pint or a catch-up with friends", + ], + "The Sheep Held Inn": [ + "Historic pub with a charming atmosphere", + "Large beer garden with stunning views of the city", + "Gastropub menu with a focus on Scottish cuisine", + "Perfect for a special occasion or a romantic evening", + ], + "The Canny Man's": [ + "Quirky and eclectic pub with a unique atmosphere", + "Wide range of international beers and spirits", + "Hidden gem with a loyal local following", + "Great for a conversation and a relaxed drink", + ], + "The Guildford Arms": [ + "Traditional pub with a Victorian interior", + "Popular with students and locals alike", + "Affordable prices and a lively atmosphere", + "Perfect for a casual night out with friends", + ], + "The Bow Bar": [ + "Wide selection of whiskies and real ales", + "Knowledgeable staff who can recommend a dram", + "Traditional pub with a relaxed atmosphere", + "Great for a whisky tasting or a quiet pint", + ], + "The Blue Moon": [ + "Historic pub with a literary connection", + "Cozy and intimate setting with a fireplace", + "Wide selection of Scottish gins and cocktails", + "Perfect for a romantic date or a special occasion", + ], + "The Ensign Ewart": [ + "Traditional pub with a military history", + "Located near Edinburgh Castle", + "Popular with tourists and locals alike", + "Great for a pint and a bite to eat after sightseeing", + ], + "The Hanging Bat": [ + "Craft beer bar with a wide selection of international beers", + "Lively atmosphere with regular events and live music", + "Great for a night out with friends", + "Popular with beer enthusiasts", + ], +} + + +SOCIAL_CONTEXT = [ + ( + "The Royal Mile is bustling with tourists and street performers." + " {players} navigate the crowds, admiring the historic buildings. The" + " sound of bagpipes fills the air. {player_name} just arrived, their" + " eyes wide with excitement." + ), + ( + "A cool mist hangs over the city as {players} climb Calton Hill. The " + "view from the top is breathtaking, with the cityscape stretching out " + "before them. They pause to catch their breath, admiring the panoramic " + "vista. {player_name} just arrived, their face flushed from the climb." + ), + ( + "The aroma of coffee fills the air in Stockbridge. {players} stroll " + "along the charming streets, browsing the independent shops and cafes. " + "They stop for a coffee and a pastry, enjoying the relaxed atmosphere. " + "{player_name} just arrived, their smile widening at the sight of the " + "quaint neighborhood." + ), + ( + "The sound of laughter echoes through the Grassmarket as {players}" + " explore the lively market stalls. They haggle for souvenirs and" + " sample local delicacies. The atmosphere is festive and vibrant." + " {player_name} just arrived, their senses bombarded with the sights," + " sounds, and smells." + ), + ( + "The sun sets over the Firth of Forth, casting a golden glow on the" + " water. {players} walk along Portobello Beach, enjoying the fresh air" + " and the sound of the waves. They find a bench and watch the sun dip" + " below the horizon. {player_name} just arrived, their heart filled" + " with a sense of peace." + ), +] + +RUGBY_COUNTRIES = [ + "South Africa", + "New Zealand", + "France", + "Ireland", + "England", + "Australia", + "Argentina", + "Wales", + "Scotland", + "Fiji", + "Japan", + "Italy", + "Samoa", + "Georgia", + "Tonga", + "Romania", + "Namibia", + "Uruguay", + "Chile", + "Portugal", +] + +FEMALE_NAMES = [ + "Ailsa MacDonald", + "Catriona Campbell", + "Fiona Stewart", + "Isla MacLeod", + "Morag MacKay", + "Shona Cameron", + "Iona Ross", + "Mhairi Wilson", + "Kirsty Robertson", + "Eilidh Davidson", +] + +MALE_NAMES = [ + "Angus Graham", + "Calum Scott", + "Douglas Reid", + "Euan Murray", + "Fraser Clark", + "Hamish Taylor", + "Iain Brown", + "Malcolm Mitchell", + "Niall Thomson", + "Rory Stewart", +] + + +def _make_empty_relationship_matrix(names: Sequence[str]): + """Samples a symmetric matrix of relationships in a group. + + Args: + names: A list of strings representing the names of individuals in the + group. + + Returns: + A dictionary representing the symmetric relationship matrix, where: + - Keys are names from the 'names' list. + - Values are dictionaries, where: + - Keys are also names from the 'names' list. + - Values are either 0.0 or 1.0, representing the relationship + between two individuals. + - The matrix is symmetric: m[a][b] == m[b][a] + - Diagonal elements are 1: m[a][a] == 1 + """ + + m = {} + for a in names: + m[a] = {} + for b in names: + if a == b: + m[a][b] = 1.0 # Diagonal elements are 1 + elif b in m and a in m[b]: + m[a][b] = m[b][a] # Ensure symmetry + else: + m[a][b] = 0.0 # No relationship + + return m + + +def sample_parameters(seed: int | None = None): + """Samples a set of parameters for the world configuration.""" + + seed = seed if seed is not None else random.getrandbits(63) + rng = random.Random(seed) + + pubs = rng.sample(list(PUB_PREFERENCES.keys()), NUM_PUBS) + pub_preferences = {k: PUB_PREFERENCES[k] for k in pubs} + + config = pub_coordination.WorldConfig( + year=YEAR, + location="Edinburgh", + event="The Rugby World Cup", + game_countries=RUGBY_COUNTRIES, + venues=pubs, + venue_preferences=pub_preferences, + social_context=SOCIAL_CONTEXT, + random_seed=seed, + pub_closed_probability=1.0, + num_games=4, + num_main_players=5, + num_supporting_players=0, + ) + + all_names = list(MALE_NAMES) + list(FEMALE_NAMES) + + rng.shuffle(all_names) + config.people = all_names + + for _, name in enumerate(MALE_NAMES): + config.person_data[name] = {"gender": "male", "favorite_pub": pubs[0]} + for _, name in enumerate(FEMALE_NAMES): + config.person_data[name] = {"gender": "female", "favorite_pub": pubs[0]} + + m = _make_empty_relationship_matrix(config.people[: config.num_main_players]) + + # Make the first two players friends. + visitor_name = config.people[0] + friend_name = config.people[1] + m[visitor_name][friend_name] = 1.0 + m[friend_name][visitor_name] = 1.0 + + # Make the rest of the players friends with everyone but the first two. + for i in config.people[2 : config.num_main_players]: + for j in config.people[2 : config.num_main_players]: + m[i][j] = 1.0 + m[j][i] = 1.0 + + config.person_data[visitor_name]["favorite_pub"] = pubs[0] + config.person_data[friend_name]["favorite_pub"] = pubs[1] + + config.relationship_matrix = m + + return config diff --git a/examples/modular/environment/modules/vegbrooke_haggling.py b/examples/modular/environment/modules/vegbrooke_haggling.py index b591b21b..2b964646 100644 --- a/examples/modular/environment/modules/vegbrooke_haggling.py +++ b/examples/modular/environment/modules/vegbrooke_haggling.py @@ -118,7 +118,10 @@ def sample_parameters(seed: int | None = None): scene_visuals=VISUAL_SCENE_OPENINGS, buyer_base_reward_min=2, seller_base_reward_max=5, + num_supporting_players=0, + num_main_players=4, random_seed=seed, + num_games=2, ) rng = random.Random(config.random_seed) diff --git a/examples/modular/environment/modules/vegbrooke_haggling_strange_game.py b/examples/modular/environment/modules/vegbrooke_haggling_strange_game.py new file mode 100644 index 00000000..fa8f025e --- /dev/null +++ b/examples/modular/environment/modules/vegbrooke_haggling_strange_game.py @@ -0,0 +1,137 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""World configuration for the Fruitville haggling scenario.""" + +import random +from examples.modular.environment import haggling + +YEAR = 1913 +MONTH = 9 +DAY = 12 + +FEMALE_NAMES = [ + "Elara Greenleaf", + "Seraphina Rootwood", + "Willow Thistlebrook", + "Ivy Mossheart", + "Rosalind Nettleford", + "Anya Pepperbloom", + "Bryony Trufflewood", + "Linnea Beetleblossom", + "Maeve Parsnipvale", + "Thora Gourdvine", + "Calla Leekmeadow", + "Esme Artichokehill", + "Freya Turniptop", + "Iris Cucumberford", + "Lyra Onionbrook", + "Nova Radishglen", + "Opal Cabbageheart", + "Saffron Sproutshade", + "Verity Celerywood", + "Wren Garlicgrove", +] + +MALE_NAMES = [ + "Cedric Willowbark", + "Rowan Mossglen", + "Finnian Thistledew", + "Asher Nettlewood", + "Jasper Peppercorn", + "Silas Trufflebrook", + "Eamon Beetlebranch", + "Gareth Parsnipfield", + "Torin Gourdwhisper", + "Callum Leekstone", + "Dorian Artichokevale", + "Evander Turnipseed", + "Griffin Cucumberpatch", + "Lysander Onionglen", + "Nolan Radishbrook", + "Oren Cabbagevine", + "Quentin Sproutmeadow", + "Tobias Celeryhill", + "Viggo Garlicstream", + "Wyatt Willowshade", +] + +SCENARIO_PREMISE = ( + "In the enchanted kingdom of Verdant, nestled among rolling hills, lies the" + " quaint town of Vegbrooke, renowned for its vibrant vegetable market." + " Merchants and travelers from across the realm journey to Vegbrooke to" + " trade and acquire the finest produce." +) + +VISUAL_SCENE_OPENINGS = [ + ( + "The first rays of dawn paint the sky with hues of orange and gold as" + " the vegetable market of Vegbrooke awakens. Merchants bustle about," + " arranging their colorful displays of crisp cabbages, plump pumpkins," + " and fragrant herbs." + ), + ( + "A gentle mist blankets the cobblestone streets of Vegbrooke as the" + " market begins to stir. The air fills with the earthy aroma of freshly" + " harvested root vegetables and the cheerful chatter of early shoppers." + ), + ( + "Sunlight filters through the leaves of the ancient oak tree that" + " stands sentinel over the market square. Farmers arrive in their" + " creaking carts, laden with baskets overflowing with vibrant produce," + " ready for a day of lively trade." + ), + ( + "The sound of cheerful bartering fills the air as the market of" + " Vegbrooke bursts into life. Shoppers eagerly inspect the mounds of" + " gleaming peppers, glistening eggplants, and artfully arranged bundles" + " of asparagus." + ), + ( + "A cool breeze carries the scent of blooming flowers from the nearby" + " meadows as the market of Vegbrooke awakens. Merchants greet each" + " other with warm smiles, preparing for another day of bustling" + " activity and the joy of sharing the bounty of the land." + ), +] + + +def sample_parameters(seed: int | None = None): + """Samples a set of parameters for the world configuration.""" + seed = seed if seed is not None else random.getrandbits(63) + + config = haggling.WorldConfig( + year=YEAR, + location="Fruitville", + premise=SCENARIO_PREMISE, + scene_visuals=VISUAL_SCENE_OPENINGS, + buyer_base_reward_min=1, + buyer_base_reward_max=1, + seller_base_reward_min=6, + seller_base_reward_max=6, + random_seed=seed, + ) + rng = random.Random(config.random_seed) + + all_names = list(MALE_NAMES) + list(FEMALE_NAMES) + + rng.shuffle(all_names) + config.people = all_names + + for _, name in enumerate(MALE_NAMES): + config.person_data[name] = {"gender": "male"} + for _, name in enumerate(FEMALE_NAMES): + config.person_data[name] = {"gender": "female"} + + return config diff --git a/examples/modular/environment/modules/vegbrooke_haggling_stubborn.py b/examples/modular/environment/modules/vegbrooke_haggling_stubborn.py new file mode 100644 index 00000000..196b9383 --- /dev/null +++ b/examples/modular/environment/modules/vegbrooke_haggling_stubborn.py @@ -0,0 +1,159 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""World configuration for the Fruitville haggling scenario.""" + +import random +from examples.modular.environment import haggling + +YEAR = 1913 +MONTH = 9 +DAY = 12 + +FEMALE_NAMES = [ + "Elara Greenleaf", + "Seraphina Rootwood", + "Willow Thistlebrook", + "Ivy Mossheart", + "Rosalind Nettleford", + "Anya Pepperbloom", + "Bryony Trufflewood", + "Linnea Beetleblossom", + "Maeve Parsnipvale", + "Thora Gourdvine", + "Calla Leekmeadow", + "Esme Artichokehill", + "Freya Turniptop", + "Iris Cucumberford", + "Lyra Onionbrook", + "Nova Radishglen", + "Opal Cabbageheart", + "Saffron Sproutshade", + "Verity Celerywood", + "Wren Garlicgrove", +] + +MALE_NAMES = [ + "Cedric Willowbark", + "Rowan Mossglen", + "Finnian Thistledew", + "Asher Nettlewood", + "Jasper Peppercorn", + "Silas Trufflebrook", + "Eamon Beetlebranch", + "Gareth Parsnipfield", + "Torin Gourdwhisper", + "Callum Leekstone", + "Dorian Artichokevale", + "Evander Turnipseed", + "Griffin Cucumberpatch", + "Lysander Onionglen", + "Nolan Radishbrook", + "Oren Cabbagevine", + "Quentin Sproutmeadow", + "Tobias Celeryhill", + "Viggo Garlicstream", + "Wyatt Willowshade", +] + +SCENARIO_PREMISE = ( + "In the enchanted kingdom of Verdant, nestled among rolling hills, lies the" + " quaint town of Vegbrooke, renowned for its vibrant vegetable market." + " Merchants and travelers from across the realm journey to Vegbrooke to" + " trade and acquire the finest produce." +) + +VISUAL_SCENE_OPENINGS = [ + ( + "The first rays of dawn paint the sky with hues of orange and gold as" + " the vegetable market of Vegbrooke awakens. Merchants bustle about," + " arranging their colorful displays of crisp cabbages, plump pumpkins," + " and fragrant herbs." + ), + ( + "A gentle mist blankets the cobblestone streets of Vegbrooke as the" + " market begins to stir. The air fills with the earthy aroma of freshly" + " harvested root vegetables and the cheerful chatter of early shoppers." + ), + ( + "Sunlight filters through the leaves of the ancient oak tree that" + " stands sentinel over the market square. Farmers arrive in their" + " creaking carts, laden with baskets overflowing with vibrant produce," + " ready for a day of lively trade." + ), + ( + "The sound of cheerful bartering fills the air as the market of" + " Vegbrooke bursts into life. Shoppers eagerly inspect the mounds of" + " gleaming peppers, glistening eggplants, and artfully arranged bundles" + " of asparagus." + ), + ( + "A cool breeze carries the scent of blooming flowers from the nearby" + " meadows as the market of Vegbrooke awakens. Merchants greet each" + " other with warm smiles, preparing for another day of bustling" + " activity and the joy of sharing the bounty of the land." + ), +] + + +def sample_parameters(seed: int | None = None): + """Samples a set of parameters for the world configuration.""" + seed = seed if seed is not None else random.getrandbits(63) + + config = haggling.WorldConfig( + year=YEAR, + location="Fruitville", + premise=SCENARIO_PREMISE, + scene_visuals=VISUAL_SCENE_OPENINGS, + buyer_base_reward_min=2, + seller_base_reward_max=5, + supporting_player_parameters={ + "fixed_response_by_call_to_action": { + "proposed 1 coin": "reject", + "proposed 2 coins": "reject", + "proposed 3 coins": "reject", + "proposed 4 coins": "accept", + "proposed 5 coins": "reject", + "What price would {name} propose?:": "4 coins", + }, + "specific_memories": [ + "{name} is a stubborn merchant. {name} will sell his items" + " exactly for 4 coins no more and no less. He is very vocal" + " about it." + ], + "explciti_preference_component": ( + "{name} is a stubborn merchant. {name} will sell his items" + " exactly for 4 coins no more and no less. He is very vocal" + " about it." + ), + }, + num_supporting_players=1, + num_main_players=1, + num_games=5, + random_seed=seed, + only_match_with_support=True, + ) + rng = random.Random(config.random_seed) + + all_names = list(MALE_NAMES) + list(FEMALE_NAMES) + + rng.shuffle(all_names) + config.people = all_names + + for _, name in enumerate(MALE_NAMES): + config.person_data[name] = {"gender": "male"} + for _, name in enumerate(FEMALE_NAMES): + config.person_data[name] = {"gender": "female"} + + return config diff --git a/examples/modular/environment/state_formation.py b/examples/modular/environment/state_formation.py index ea39e5a4..798a1fa6 100644 --- a/examples/modular/environment/state_formation.py +++ b/examples/modular/environment/state_formation.py @@ -455,7 +455,9 @@ def configure_scenes( 'intend to spend on each of the ' f'following activities: {activities_str}. Note that ' 'proportions of time should sum to 1. Sleep counts ' - f'as {config.free_time_activity}.'), + f'as {config.free_time_activity} and, ' + 'all else equal, ' + 'more leisure time is better.'), tag='announcement', ), override_game_master=no_conversation_game_master, @@ -1072,7 +1074,7 @@ def __init__( randomise_initiative=True, name='Negotiation scene', review_participants=False, - max_steps=1, + max_steps=4, memory=self._game_master_memory, additional_components=[agreement_component], verbose=True, diff --git a/examples/modular/environment/supporting_agent_factory/observe_and_summarize_agent.py b/examples/modular/environment/supporting_agent_factory/observe_and_summarize_agent.py new file mode 100644 index 00000000..dc8514b7 --- /dev/null +++ b/examples/modular/environment/supporting_agent_factory/observe_and_summarize_agent.py @@ -0,0 +1,141 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An Agent Factory.""" + +import datetime + +from concordia.agents import entity_agent_with_logging +from concordia.associative_memory import associative_memory +from concordia.associative_memory import formative_memories +from concordia.clocks import game_clock +from concordia.components import agent as agent_components +from concordia.contrib.components.agent import observations_since_last_update +from concordia.contrib.components.agent import situation_representation_via_narrative +from concordia.language_model import language_model +from concordia.memory_bank import legacy_associative_memory +from concordia.utils import measurements as measurements_lib + + +def _get_class_name(object_: object) -> str: + return object_.__class__.__name__ + + +def build_agent( + *, + config: formative_memories.AgentConfig, + model: language_model.LanguageModel, + memory: associative_memory.AssociativeMemory, + clock: game_clock.MultiIntervalClock, + update_time_interval: datetime.timedelta | None = None, +) -> entity_agent_with_logging.EntityAgentWithLogging: + """Build an agent. + + Args: + config: The agent config to use. + model: The language model to use. + memory: The agent's memory object. + clock: The clock to use. + update_time_interval: Agent calls update every time this interval passes. + + Returns: + An agent. + """ + del update_time_interval + if config.extras.get('main_character', False): + raise ValueError('This function is meant for a supporting character ' + 'but it was called on a main character.') + + agent_name = config.name + + raw_memory = legacy_associative_memory.AssociativeMemoryBank(memory) + + measurements = measurements_lib.Measurements() + instructions = agent_components.instructions.Instructions( + agent_name=agent_name, + logging_channel=measurements.get_channel('Instructions').on_next, + ) + + time_display = agent_components.report_function.ReportFunction( + function=clock.current_time_interval_str, + pre_act_key='\nCurrent time', + logging_channel=measurements.get_channel('TimeDisplay').on_next, + ) + + observation_label = '\nObservation' + observation = observations_since_last_update.ObservationsSinceLastUpdate( + model=model, + clock_now=clock.now, + pre_act_key=observation_label, + logging_channel=measurements.get_channel( + 'ObservationsSinceLastUpdate').on_next, + ) + + situation_representation_label = ( + f'\nQuestion: What situation is {agent_name} in right now?\nAnswer') + situation_representation = ( + situation_representation_via_narrative.SituationRepresentation( + model=model, + clock_now=clock.now, + pre_act_key=situation_representation_label, + logging_channel=measurements.get_channel( + 'SituationRepresentation' + ).on_next, + ) + ) + + if config.goal: + goal_label = '\nOverarching goal' + overarching_goal = agent_components.constant.Constant( + state=config.goal, + pre_act_key=goal_label, + logging_channel=measurements.get_channel(goal_label).on_next) + else: + goal_label = None + overarching_goal = None + + entity_components = ( + # Components that provide pre_act context. + instructions, + time_display, + situation_representation, + observation, + ) + components_of_agent = {_get_class_name(component): component + for component in entity_components} + components_of_agent[ + agent_components.memory_component.DEFAULT_MEMORY_COMPONENT_NAME] = ( + agent_components.memory_component.MemoryComponent(raw_memory)) + + component_order = list(components_of_agent.keys()) + if overarching_goal is not None: + components_of_agent[goal_label] = overarching_goal + # Place goal after the instructions. + component_order.insert(1, goal_label) + + act_component = agent_components.concat_act_component.ConcatActComponent( + model=model, + clock=clock, + component_order=component_order, + logging_channel=measurements.get_channel('ActComponent').on_next, + ) + + agent = entity_agent_with_logging.EntityAgentWithLogging( + agent_name=agent_name, + act_component=act_component, + context_components=components_of_agent, + component_logging=measurements, + ) + + return agent diff --git a/examples/modular/launch_concordia_challenge_evaluation.py b/examples/modular/launch_concordia_challenge_evaluation.py index 407f1893..038a3aa3 100644 --- a/examples/modular/launch_concordia_challenge_evaluation.py +++ b/examples/modular/launch_concordia_challenge_evaluation.py @@ -67,6 +67,7 @@ """ import argparse +from collections.abc import Sequence import datetime import functools import importlib @@ -230,7 +231,7 @@ def _evaluate_one_repetition( def _evaluate_all_repetitions_on_one_scenario( scenario_name: str, scenario_config: scenarios_lib.ScenarioConfig, -) -> logging_lib.ScenarioResult: +) -> Sequence[logging_lib.ScenarioResult]: """Evaluates the agent on one scenario, averaging over repetitions. Args: @@ -243,11 +244,6 @@ def _evaluate_all_repetitions_on_one_scenario( """ print(f'Running scenario: {scenario_name}') # Run several simulations per scenario - simulation_outcomes = [] - focal_per_capita_scores_to_average = [] - background_per_capita_scores_to_average = [] - ungrouped_per_capita_scores_to_average = [] - tasks_this_scenario = { str(i): functools.partial( _evaluate_one_repetition, @@ -267,6 +263,7 @@ def _evaluate_all_repetitions_on_one_scenario( 'Raised errors', list(exceptions_per_repetition.values()) ) + scenario_results = [] for repetition_idx, outcome in outputs_per_repetition.items(): if scenario_config.focal_is_resident: focal_scores = list(outcome.resident_scores.values()) @@ -279,45 +276,38 @@ def _evaluate_all_repetitions_on_one_scenario( # Calculate per capita scores. print(f'\nScores for repetition {repetition_idx}:') focal_per_capita_score = np.mean(focal_scores) - focal_per_capita_scores_to_average.append(focal_per_capita_score) print(f' Focal per capita score: {focal_per_capita_score}') background_per_capita_score = np.mean(background_scores) - background_per_capita_scores_to_average.append(background_per_capita_score) print(f' Background per capita score: {background_per_capita_score}') ungrouped_per_capita_score = np.mean(ungrouped_scores) - ungrouped_per_capita_scores_to_average.append(ungrouped_per_capita_score) print(f' Ungrouped per capita score: {ungrouped_per_capita_score}') - # Average scores over repetitions and save results for all repetitions in a - # json-serializable format. - scenario_result_ = logging_lib.ScenarioResult( - scenario=scenario_name, - focal_agent=args.agent_name, - background_agent=scenario_config.background_agent_module, - focal_per_capita_score=np.mean(focal_per_capita_scores_to_average), - background_per_capita_score=np.mean( - background_per_capita_scores_to_average - ), - ungrouped_per_capita_score=np.mean( - ungrouped_per_capita_scores_to_average - ), - simulation_outcomes=tuple(simulation_outcomes), - focal_is_resident=scenario_config.focal_is_resident, - api_type=args.api_type, - model=args.model_name, - embedder=args.embedder_name, - disable_language_model=args.disable_language_model, - exclude_from_elo_calculation=args.exclude_from_elo_calculation, - ) - scenario_json_filename = ( - f'{args.agent_name}__{args.model_name}__' - f'{args.embedder_name}__only_{scenario_name}.json' - ).replace('/', '_') - scenario_json_filename = os.path.join(results_dir, scenario_json_filename) - json_str_ = scenario_result_.to_json() - with open(scenario_json_filename, 'a', encoding='utf-8') as f: - f.write(json_str_) - return scenario_result_ + scenario_result_ = logging_lib.ScenarioResult( + scenario=scenario_name, + repetition_idx=repetition_idx, + focal_agent=args.agent_name, + background_agent=scenario_config.background_agent_module, + focal_per_capita_score=focal_per_capita_score, + background_per_capita_score=background_per_capita_score, + ungrouped_per_capita_score=ungrouped_per_capita_score, + simulation_outcome=outcome, + focal_is_resident=scenario_config.focal_is_resident, + api_type=args.api_type, + model=args.model_name, + embedder=args.embedder_name, + disable_language_model=args.disable_language_model, + exclude_from_elo_calculation=args.exclude_from_elo_calculation, + ) + scenario_json_filename = ( + f'{args.agent_name}__{args.model_name}__' + f'{args.embedder_name}__only__{scenario_name}__{repetition_idx}.json' + ).replace('/', '_') + scenario_json_filename = os.path.join(results_dir, scenario_json_filename) + json_str_ = scenario_result_.to_json() + with open(scenario_json_filename, 'a', encoding='utf-8') as f: + f.write(json_str_) + scenario_results.append(scenario_result_) + return scenario_results tasks = { name: functools.partial( @@ -330,16 +320,19 @@ def _evaluate_all_repetitions_on_one_scenario( evaluation_results = concurrency.run_tasks(tasks) # Save evaluation results for all scenarios with this agent to one json file. +num_expected_results = (len(scenarios_lib.SCENARIO_CONFIGS) * + args.num_repetitions_per_scenario) json_filename = ( f'{args.agent_name}__{args.model_name}__{args.embedder_name}.json' ).replace('/', '_') idx = 0 with open(json_filename, 'a', encoding='utf-8') as file_handle: file_handle.write('[\n') - for scenario_name_, scenario_result in evaluation_results.items(): - json_str = evaluation_results[scenario_name_].to_json() - if idx < len(scenarios_lib.SCENARIO_CONFIGS) - 1: - json_str += ',\n' - file_handle.write(json_str) - idx += 1 + for scenario_name_, _ in evaluation_results.items(): + for scenario_result in evaluation_results[scenario_name_]: + json_str = scenario_result.to_json() + if idx < num_expected_results - 1: + json_str += ',\n' + file_handle.write(json_str) + idx += 1 file_handle.write('\n]') diff --git a/examples/modular/launch_one_scenario.py b/examples/modular/launch_one_scenario.py index 2bd813af..3fd58b90 100644 --- a/examples/modular/launch_one_scenario.py +++ b/examples/modular/launch_one_scenario.py @@ -188,10 +188,6 @@ print(f'Running scenario: {args.scenario_name}') scenario_config = scenarios_lib.SCENARIO_CONFIGS[args.scenario_name] # Run several simulations per scenario -simulation_outcomes = [] -focal_per_capita_scores_to_average = [] -background_per_capita_scores_to_average = [] -ungrouped_per_capita_scores_to_average = [] for repetition_idx in range(args.num_repetitions_per_scenario): measurements = measurements_lib.Measurements() runnable_simulation = scenarios_lib.build_simulation( @@ -205,7 +201,6 @@ ) # Run the simulation outcome, text_results_log = runnable_simulation() - simulation_outcomes.append(outcome) if scenario_config.focal_is_resident: focal_scores = list(outcome.resident_scores.values()) background_scores = list(outcome.visitor_scores.values()) @@ -217,13 +212,10 @@ # Calculate per capita scores. print('\nScores:') focal_per_capita_score = np.mean(focal_scores) - focal_per_capita_scores_to_average.append(focal_per_capita_score) print(f' Focal per capita score: {focal_per_capita_score}') background_per_capita_score = np.mean(background_scores) - background_per_capita_scores_to_average.append(background_per_capita_score) print(f' Background per capita score: {background_per_capita_score}') ungrouped_per_capita_score = np.mean(ungrouped_scores) - ungrouped_per_capita_scores_to_average.append(ungrouped_per_capita_score) print(f' Ungrouped per capita score: {ungrouped_per_capita_score}') # Write the full text log as an HTML file in the current working directory. html_filename = ( @@ -234,29 +226,27 @@ with open(html_filename, 'a', encoding='utf-8') as f: f.write(text_results_log) -# Average scores over repetitions and save results for all repetitions in a -# json-serializable format. -scenario_result = logging_lib.ScenarioResult( - scenario=args.scenario_name, - focal_agent=args.agent_name, - background_agent=scenario_config.background_agent_module, - focal_per_capita_score=np.mean(focal_per_capita_scores_to_average), - background_per_capita_score=np.mean( - background_per_capita_scores_to_average - ), - ungrouped_per_capita_score=np.mean(ungrouped_per_capita_scores_to_average), - simulation_outcomes=tuple(simulation_outcomes), - focal_is_resident=scenario_config.focal_is_resident, - api_type=args.api_type, - model=args.model_name, - embedder=args.embedder_name, - disable_language_model=args.disable_language_model, - exclude_from_elo_calculation=args.exclude_from_elo_calculation, -) -scenario_json_filename = ( - f'{args.agent_name}__{args.model_name}__' - f'{args.embedder_name}__only_{args.scenario_name}.json' -).replace('/', '_') -json_str_ = scenario_result.to_json() -with open(scenario_json_filename, 'a', encoding='utf-8') as f: - f.write(json_str_) + scenario_result = logging_lib.ScenarioResult( + scenario=args.scenario_name, + repetition_idx=str(repetition_idx), + focal_agent=args.agent_name, + background_agent=scenario_config.background_agent_module, + focal_per_capita_score=focal_per_capita_score, + background_per_capita_score=background_per_capita_score, + ungrouped_per_capita_score=ungrouped_per_capita_score, + simulation_outcome=outcome, + focal_is_resident=scenario_config.focal_is_resident, + api_type=args.api_type, + model=args.model_name, + embedder=args.embedder_name, + disable_language_model=args.disable_language_model, + exclude_from_elo_calculation=args.exclude_from_elo_calculation, + ) + scenario_json_filename = ( + f'{args.agent_name}__{args.model_name}__' + f'{args.embedder_name}__only__{args.scenario_name}__{repetition_idx}' + '.json' + ).replace('/', '_') + json_str_ = scenario_result.to_json() + with open(scenario_json_filename, 'a', encoding='utf-8') as f: + f.write(json_str_) diff --git a/examples/modular/utils/logging_types.py b/examples/modular/utils/logging_types.py index f07fe899..600609a4 100644 --- a/examples/modular/utils/logging_types.py +++ b/examples/modular/utils/logging_types.py @@ -39,17 +39,17 @@ class SimulationOutcome: @dataclasses.dataclass(frozen=True, kw_only=True) class ScenarioResult: """Result from testing a single agent on several repetitions of a scenario. - + Attributes: scenario: The name of the scenario. + repetition_idx: The index of the repetition (i.e. the seed). focal_agent: The name of the agent that is being tested in the focal slots. background_agent: The name of the agent used in the background player slots. focal_per_capita_score: The per capita score of the focal agent. background_per_capita_score: The per capita score of the background agent. ungrouped_per_capita_score: The per capita score of the focal agent, averaged over all players (both residents and visitors). - simulation_outcomes: A tuple of SimulationOutcomes, one for each repetition - of the scenario. + simulation_outcome: A SimulationOutcome object. focal_is_resident: Whether the focal agent is a resident or a visitor. api_type: The API type used for the simulation (e.g. `google_aistudio_model`, `mistral`, `openai`, etc). @@ -64,6 +64,7 @@ class ScenarioResult: """ scenario: str + repetition_idx: str focal_agent: str background_agent: str @@ -72,9 +73,7 @@ class ScenarioResult: background_per_capita_score: float ungrouped_per_capita_score: float - simulation_outcomes: tuple[SimulationOutcome, ...] = dataclasses.field( - repr=False - ) + simulation_outcome: SimulationOutcome = dataclasses.field(repr=False) focal_is_resident: bool @@ -87,16 +86,13 @@ class ScenarioResult: def to_json(self) -> str: """Encode this dataclass as a string to serialize as a json file.""" - simulation_outcome_dicts = [] - for outcome in self.simulation_outcomes: - outcome_dict = dataclasses.asdict(outcome) - outcome_dict['resident_scores'] = dict(outcome_dict['resident_scores']) - outcome_dict['visitor_scores'] = dict(outcome_dict['visitor_scores']) - outcome_dict['metadata'] = dict(outcome_dict['metadata']) - simulation_outcome_dicts.append(outcome_dict) + outcome_dict = dataclasses.asdict(self.simulation_outcome) + outcome_dict['resident_scores'] = dict(outcome_dict['resident_scores']) + outcome_dict['visitor_scores'] = dict(outcome_dict['visitor_scores']) + outcome_dict['metadata'] = dict(outcome_dict['metadata']) self_as_dict = dataclasses.asdict(self) - self_as_dict['simulation_outcomes'] = tuple(simulation_outcome_dicts) + self_as_dict['simulation_outcome'] = outcome_dict return json.dumps(self_as_dict, indent=2) diff --git a/setup.py b/setup.py index 9c650794..26bd1711 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def _remove_excluded(description: str) -> str: setuptools.setup( name='gdm-concordia', - version='1.8.8', + version='1.8.9', license='Apache 2.0', license_files=['LICENSE'], url='https://github.com/google-deepmind/concordia', @@ -61,6 +61,7 @@ def _remove_excluded(description: str) -> str: 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Scientific/Engineering :: Artificial Intelligence', ], packages=setuptools.find_packages(include=['concordia', 'concordia.*']), @@ -78,7 +79,7 @@ def _remove_excluded(description: str) -> str: 'numpy', 'ollama', 'openai>=1.3.0', - 'pandas<=2.0.3', + 'pandas', 'python-dateutil', 'reactivex', 'retry',