Skip to content

Commit

Permalink
Add to unstable: logging, switch_act, and event_resolution component …
Browse files Browse the repository at this point in the history
…using the same thought_chain for event resolution that we already use in the stable game master.

PiperOrigin-RevId: 722650193
Change-Id: I6d5a9d9c7f5cf8012eb03f9ef44b17e8350054ae
  • Loading branch information
jzleibo authored and copybara-github committed Feb 3, 2025
1 parent 544ae66 commit 221c866
Show file tree
Hide file tree
Showing 12 changed files with 503 additions and 67 deletions.
1 change: 1 addition & 0 deletions concordia/components/game_master/unstable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@

"""Library of components specifically for generative game masters."""

from concordia.components.game_master.unstable import event_resolution
from concordia.components.game_master.unstable import instructions
from concordia.components.game_master.unstable import switch_act
123 changes: 123 additions & 0 deletions concordia/components/game_master/unstable/event_resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Copyright 2023 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.

"""Component that helps a game master decide whose turn is next."""

from collections.abc import Callable, Mapping, Sequence
import types

from concordia.components.agent import action_spec_ignored
from concordia.document import interactive_document
from concordia.language_model import language_model
from concordia.thought_chains import thought_chains
from concordia.typing import entity as entity_lib
from concordia.typing import entity_component
from concordia.typing import logging


DEFAULT_RESOLUTION_PRE_ACT_KEY = '\nEvent'

GET_ACTIVE_ENTITY_QUERY = ('Which entity just took an action?'
' Respond using only the entity\'s name and no'
' other words.')
GET_PUTATIVE_ACTION_QUERY = 'What is {name} attempting to do?'


class EventResolution(entity_component.ContextComponent):
"""A component that resolves and records events.
"""

def __init__(
self,
model: language_model.LanguageModel,
event_resolution_steps: (
Sequence[
Callable[
[interactive_document.InteractiveDocument, str, str], str
]
]
| None
) = None,
components: Mapping[
entity_component.ComponentName, str
] = types.MappingProxyType({}),
pre_act_key: str = DEFAULT_RESOLUTION_PRE_ACT_KEY,
logging_channel: logging.LoggingChannel = logging.NoOpLoggingChannel,
):
"""Initializes the component.
Args:
model: The language model to use for the component.
event_resolution_steps: thinking steps for the event resolution component
to use whenever it converts putative events like action suggestions into
real events in the simulation.
components: The components to condition the answer on. This is a mapping
of the component name to a label to use in the prompt.
pre_act_key: Prefix to add to the output of the component when called
in `pre_act`.
logging_channel: The channel to use for debug logging.
Raises:
ValueError: If the component order is not None and contains duplicate
components.
"""
super().__init__()
self._model = model
self._event_resolution_steps = event_resolution_steps
self._components = dict(components)
self._pre_act_key = pre_act_key
self._logging_channel = logging_channel

def get_named_component_pre_act_value(self, component_name: str) -> str:
"""Returns the pre-act value of a named component of the parent entity."""
return (
self.get_entity().get_component(
component_name, type_=action_spec_ignored.ActionSpecIgnored
).get_pre_act_value()
)

def pre_act(
self,
action_spec: entity_lib.ActionSpec,
) -> str:
result = ''
prompt_to_log = ''
if action_spec.output_type == entity_lib.OutputType.RESOLVE:
entity_name = self.get_entity().name
prompt = interactive_document.InteractiveDocument(self._model)
component_states = '\n'.join([
f"{entity_name}'s"
f' {prefix}:\n{self.get_named_component_pre_act_value(key)}'
for key, prefix in self._components.items()
])
prompt.statement(f'Statements:\n{component_states}\n')
active_entity_name = prompt.open_question(
GET_ACTIVE_ENTITY_QUERY)
putative_action = prompt.open_question(
GET_PUTATIVE_ACTION_QUERY.format(name=active_entity_name),
max_tokens=1200)
prompt, event_statement = thought_chains.run_chain_of_thought(
thoughts=self._event_resolution_steps,
premise=putative_action,
document=prompt,
active_player_name=active_entity_name,
)
result = f'{self._pre_act_key}: {event_statement}'
prompt_to_log = prompt.view().text()

self._logging_channel(
{'Key': self._pre_act_key,
'Value': result,
'Prompt': prompt_to_log})
return result
93 changes: 71 additions & 22 deletions concordia/components/game_master/unstable/switch_act.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@

DEFAULT_PRE_ACT_KEY = 'Act'

DEFAULT_TERMINATE_COMPONENT_NAME = '__terminate__'
DEFAULT_MAKE_OBSERVATION_COMPONENT_NAME = '__make_observation__'
DEFAULT_NEXT_ACTING_COMPONENT_NAME = '__next_acting__'
DEFAULT_NEXT_ACTION_SPEC_COMPONENT_NAME = '__next_action_spec__'
DEFAULT_RESOLUTION_COMPONENT_NAME = '__resolution__'


class SwitchAct(entity_component.ActingComponent):
"""A component which calls the appropriate method for each action type.
Expand Down Expand Up @@ -108,68 +114,109 @@ def _terminate(
self,
contexts: entity_component.ComponentContextMapping,
action_spec: entity_lib.ActionSpec) -> str:
return 'No'
context = self._context_for_action(contexts)
if DEFAULT_TERMINATE_COMPONENT_NAME in contexts:
result = str(contexts[DEFAULT_TERMINATE_COMPONENT_NAME])
self._log(result, context)
else:
# YOLO case
chain_of_thought = interactive_document.InteractiveDocument(self._model)
chain_of_thought.statement(context)
termination_bool = chain_of_thought.yes_no_question(
question=action_spec.call_to_action)
if termination_bool:
result = 'Yes'
else:
result = 'No'
self._log(result, chain_of_thought)

return result

def _make_observation(
self,
contexts: entity_component.ComponentContextMapping,
action_spec: entity_lib.ActionSpec) -> str:
return ('{name} is standing in an open field west of a white house, '
'with a boarded front door. There is a small mailbox here.')
context = self._context_for_action(contexts)
if DEFAULT_MAKE_OBSERVATION_COMPONENT_NAME in contexts:
result = str(contexts[DEFAULT_MAKE_OBSERVATION_COMPONENT_NAME])
self._log(result, context)
else:
# YOLO case
chain_of_thought = interactive_document.InteractiveDocument(self._model)
chain_of_thought.statement(context)
result = chain_of_thought.open_question(
question=action_spec.call_to_action,
max_tokens=1000)
self._log(result, chain_of_thought)

return result

def _next_acting(
self,
contexts: entity_component.ComponentContextMapping,
action_spec: entity_lib.ActionSpec) -> str:
context = self._context_for_action(contexts)
if 'Initiative:' in contexts:
return str(contexts['Initiative:'])
if DEFAULT_NEXT_ACTING_COMPONENT_NAME in contexts:
result = str(contexts[DEFAULT_NEXT_ACTING_COMPONENT_NAME])
self._log(result, context)
else:
# YOLO case
chain_of_thought = interactive_document.InteractiveDocument(self._model)
chain_of_thought.statement(context)
next_entity_index = chain_of_thought.multiple_choice_question(
question='Who is next?', answers=self._entity_names)
return self._entity_names[next_entity_index]
question=action_spec.call_to_action,
answers=self._entity_names)
result = self._entity_names[next_entity_index]
self._log(result, chain_of_thought)

return result

def _next_entity_action_spec(
self,
contexts: entity_component.ComponentContextMapping,
action_spec: entity_lib.ActionSpec) -> str:
context = self._context_for_action(contexts)
if 'next_action_spec' in contexts:
if DEFAULT_NEXT_ACTION_SPEC_COMPONENT_NAME in contexts:
# action_spec_string = _convert_to_string(
# next_action_spec['scene_type'].action_spec)
return ''
result = ''
self._log(result, context)
else:
# YOLO case
chain_of_thought = interactive_document.InteractiveDocument(self._model)
chain_of_thought.statement(context)
_ = chain_of_thought.open_question(
question='Who is next to act and what kind of decision do they face?')
_ = chain_of_thought.open_question(question=action_spec.call_to_action)
# Then ask the GM to reformat their answer in whatever string format can
# be used by the engine and its parser.
chain_of_thought.statement(
'Example formatted action specs:\n"type: free"\n"type: choice"')
'Example formatted action specs:\n"type: free"\n'
'"type: choice options: x, y, z"')
next_action_spec_string = chain_of_thought.open_question(
question='Format the decision type as an action spec.')
question='Format the decision prompt type as an action spec.')
if 'type:' not in next_action_spec_string:
next_action_spec_string = 'type: free' + next_action_spec_string
return next_action_spec_string

result = next_action_spec_string
self._log(result, chain_of_thought)

return result

def _resolve(
self,
contexts: entity_component.ComponentContextMapping,
action_spec: entity_lib.ActionSpec) -> str:
if 'resolution' in contexts:
return contexts['resolution']
context = self._context_for_action(contexts)
if DEFAULT_RESOLUTION_COMPONENT_NAME in contexts:
result = contexts[DEFAULT_RESOLUTION_COMPONENT_NAME]
self._log(result, context)
else:
chain_of_thought = interactive_document.InteractiveDocument(self._model)
context = self._context_for_action(contexts)
chain_of_thought.statement(context)
resolution = chain_of_thought.open_question(
question='As a result of the above, what happens next?')
return resolution
result = chain_of_thought.open_question(
question=action_spec.call_to_action)
self._log(result, chain_of_thought)

return result

@override
def get_action_attempt(
Expand Down Expand Up @@ -239,11 +286,13 @@ def get_action_attempt(

def _log(self,
result: str,
prompt: interactive_document.InteractiveDocument):
prompt: str | interactive_document.InteractiveDocument):
if isinstance(prompt, interactive_document.InteractiveDocument):
prompt = prompt.view().text().splitlines()
self._logging_channel({
'Key': self._pre_act_key,
'Value': result,
'Prompt': prompt.view().text().splitlines(),
'Prompt': prompt,
})

def get_state(self) -> entity_component.ComponentState:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
"""A component for representing the current situation via narrative.
"""

from collections.abc import Callable, Sequence
from collections.abc import Callable, Mapping, Sequence
import datetime
import types

from absl import logging as absl_logging
from concordia.components import agent as agent_components
Expand Down Expand Up @@ -77,6 +78,10 @@ def __init__(
memory_component_name: str = (
memory_component.DEFAULT_MEMORY_COMPONENT_NAME
),
components: Mapping[
entity_component.ComponentName, str
] = types.MappingProxyType({}),
declare_entity_as_protagonist: bool = True,
pre_act_key: str = 'The current situation',
logging_channel: logging.LoggingChannel = logging.NoOpLoggingChannel,
):
Expand All @@ -87,6 +92,10 @@ def __init__(
clock_now: Function that returns the current time.
memory_component_name: The name of the memory component from which to
retrieve related memories.
components: Components to condition the narrative on. This is a mapping
of the component name to a label to use in the prompt.
declare_entity_as_protagonist: Whether to declare the entity to be the
protagonist in the prompt.
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.
Expand All @@ -95,11 +104,25 @@ def __init__(
self._model = model
self._clock_now = clock_now
self._memory_component_name = memory_component_name
self._components = dict(components)
self._declare_entity_as_protagonist = declare_entity_as_protagonist
self._logging_channel = logging_channel

self._previous_time = None
self._situation_thus_far = None

def _add_components_if_any(
self,
chain_of_thought: interactive_document.InteractiveDocument,
) -> None:
"""Adds components to the chain of thought if any are present."""
if self._components:
component_states = '\n'.join([
f'{prefix}:\n{self.get_named_component_pre_act_value(key)}'
for key, prefix in self._components.items()
])
chain_of_thought.statement(f'Considerations:\n{component_states}\n')

def _make_pre_act_value(self) -> str:
"""Returns a representation of the current situation to pre act."""
agent_name = self.get_entity().name
Expand All @@ -113,9 +136,11 @@ def _make_pre_act_value(self) -> str:
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}')
if self._declare_entity_as_protagonist:
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}')
self._add_components_if_any(chain_of_thought)
chain_of_thought.statement(f'Events continue after {current_time}')
self._situation_thus_far = chain_of_thought.open_question(
question=(
Expand Down Expand Up @@ -143,10 +168,14 @@ def _make_pre_act_value(self) -> str:
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._add_components_if_any(chain_of_thought)
if self._declare_entity_as_protagonist:
chain_of_thought.statement(f'Protagonist: {agent_name}')
chain_of_thought.statement(
f'Thoughts and memories of {agent_name}:\n{result}'
)
else:
chain_of_thought.statement(f'Notes:\n{result}')
self._situation_thus_far = chain_of_thought.open_question(
question=(
'What situation does the protagonist find themselves in? '
Expand Down
Loading

0 comments on commit 221c866

Please sign in to comment.