Skip to content

Commit 0b72d64

Browse files
DavidePagliericopybara-github
authored andcommitted
Sequential questionnaire
PiperOrigin-RevId: 801704312 Change-Id: Ia108b196fb09f236cc0a2bf30060cd9c75715b80
1 parent 67881f5 commit 0b72d64

2 files changed

Lines changed: 303 additions & 1 deletion

File tree

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# Copyright 2025 DeepMind Technologies Limited.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Engine for running questionnaires sequentially across multiple entities."""
16+
17+
from collections.abc import Mapping, Sequence
18+
from concurrent import futures
19+
import functools
20+
import json
21+
from typing import Any, Callable, List, Tuple, cast
22+
23+
from concordia.agents import entity_agent
24+
from concordia.components.game_master import event_resolution as event_resolution_components
25+
from concordia.components.game_master import make_observation as make_observation_component
26+
from concordia.components.game_master import next_acting as next_acting_components
27+
from concordia.environment import engine as engine_lib
28+
from concordia.typing import entity as entity_lib
29+
from concordia.utils import concurrency
30+
import termcolor
31+
from typing_extensions import override
32+
33+
PUTATIVE_EVENT_TAG = event_resolution_components.PUTATIVE_EVENT_TAG
34+
EVENT_TAG = event_resolution_components.EVENT_TAG
35+
36+
DEFAULT_CALL_TO_CHECK_TERMINATION = 'Is the game/simulation finished?'
37+
DEFAULT_CALL_TO_NEXT_GAME_MASTER = (
38+
'Which rule set should we use for the next step?'
39+
)
40+
DEFAULT_CALL_TO_NEXT_ACTING = next_acting_components.DEFAULT_CALL_TO_NEXT_ACTING
41+
DEFAULT_CALL_TO_NEXT_ACTION_SPEC = (
42+
next_acting_components.DEFAULT_CALL_TO_NEXT_ACTION_SPEC
43+
)
44+
DEFAULT_CALL_TO_MAKE_OBSERVATION = (
45+
make_observation_component.DEFAULT_CALL_TO_MAKE_OBSERVATION
46+
)
47+
48+
_PRINT_COLOR = 'cyan'
49+
50+
51+
class SequentialQuestionnaireEngine(engine_lib.Engine):
52+
"""Engine for asking questions to all entities sequentially."""
53+
54+
def __init__(
55+
self,
56+
call_to_check_termination: str = DEFAULT_CALL_TO_CHECK_TERMINATION,
57+
call_to_make_observation: str = DEFAULT_CALL_TO_MAKE_OBSERVATION,
58+
call_to_next_acting: str = DEFAULT_CALL_TO_NEXT_ACTING,
59+
call_to_next_action_spec: str = DEFAULT_CALL_TO_NEXT_ACTION_SPEC,
60+
call_to_next_game_master: str = DEFAULT_CALL_TO_NEXT_GAME_MASTER,
61+
max_workers: int | None = None,
62+
):
63+
"""Constructor."""
64+
self._call_to_check_termination = call_to_check_termination
65+
self._call_to_next_acting = call_to_next_acting
66+
self._call_to_next_action_spec = call_to_next_action_spec
67+
self._call_to_next_game_master = call_to_next_game_master
68+
self._call_to_make_observation = call_to_make_observation
69+
self._max_workers = max_workers
70+
if self._max_workers is None:
71+
self._executor = None
72+
else:
73+
self._executor = futures.ThreadPoolExecutor(max_workers=self._max_workers)
74+
75+
def get_executor(self) -> futures.ThreadPoolExecutor | None:
76+
return self._executor
77+
78+
@override
79+
def next_acting(
80+
self,
81+
game_master: entity_lib.Entity,
82+
entities: Sequence[entity_lib.Entity],
83+
) -> Sequence[entity_lib.Entity]: # pytype: disable=signature-mismatch
84+
"""Returns entities that should act next."""
85+
entities_by_name = {entity.name: entity for entity in entities}
86+
87+
player_names_str = game_master.act(
88+
action_spec=entity_lib.ActionSpec(
89+
call_to_action=self._call_to_next_acting,
90+
output_type=entity_lib.OutputType.NEXT_ACTING,
91+
options=tuple(entities_by_name.keys()),
92+
)
93+
)
94+
95+
next_entity_names = player_names_str.split(',')
96+
next_entities = [
97+
entities_by_name[name]
98+
for name in next_entity_names
99+
if name in entities_by_name
100+
]
101+
return next_entities
102+
103+
def next_action_spec(
104+
self,
105+
game_master: entity_lib.Entity,
106+
acting_entities: Sequence[entity_lib.Entity],
107+
) -> List[Tuple[str, str, str]]:
108+
"""Returns the next action spec for all questions for the acting entities."""
109+
if not acting_entities:
110+
return []
111+
112+
player_names = ','.join([entity.name for entity in acting_entities])
113+
question_specs_json = game_master.act(
114+
action_spec=entity_lib.ActionSpec(
115+
call_to_action=player_names, # Pass player names here
116+
output_type=entity_lib.OutputType.NEXT_ACTION_SPEC,
117+
)
118+
)
119+
120+
question_specs_list = json.loads(question_specs_json)
121+
122+
all_action_specs: List[Tuple[str, str, str]] = []
123+
for item in question_specs_list:
124+
player_name = item['player_name']
125+
q_id = item['question_id']
126+
spec_str = item['action_spec_str']
127+
all_action_specs.append((player_name, q_id, spec_str))
128+
129+
return all_action_specs
130+
131+
def terminate(
132+
self, game_master: entity_lib.Entity, verbose: bool = False
133+
) -> bool:
134+
"""Decide if the episode should terminate."""
135+
should_terminate_string = game_master.act(
136+
action_spec=entity_lib.ActionSpec(
137+
call_to_action=self._call_to_check_termination,
138+
output_type=entity_lib.OutputType.TERMINATE,
139+
options=tuple(entity_lib.BINARY_OPTIONS.values()),
140+
)
141+
)
142+
if verbose:
143+
print(
144+
termcolor.colored(
145+
f'Terminate? {should_terminate_string}', _PRINT_COLOR
146+
)
147+
)
148+
return should_terminate_string == entity_lib.BINARY_OPTIONS['affirmative']
149+
150+
def make_observation(
151+
self,
152+
game_master: entity_lib.Entity,
153+
entity: entity_lib.Entity,
154+
verbose: bool = False,
155+
) -> str:
156+
"""Make an observation for a game object."""
157+
observation = game_master.act(
158+
action_spec=entity_lib.ActionSpec(
159+
call_to_action=self._call_to_make_observation.format(
160+
name=entity.name
161+
),
162+
output_type=entity_lib.OutputType.MAKE_OBSERVATION,
163+
)
164+
)
165+
if verbose:
166+
print(
167+
termcolor.colored(
168+
f'Observation: {observation} for {entity.name}', _PRINT_COLOR
169+
)
170+
)
171+
return observation
172+
173+
@override
174+
def run_loop(
175+
self,
176+
game_masters: Sequence[entity_lib.Entity | entity_lib.EntityWithLogging],
177+
entities: Sequence[entity_lib.Entity | entity_lib.EntityWithLogging],
178+
premise: str = '',
179+
max_steps: int = 1, # Usually only needs 1 step
180+
verbose: bool = False,
181+
log: list[Mapping[str, Any]] | None = None,
182+
checkpoint_callback: Callable[[int], None] | None = None,
183+
):
184+
if not game_masters:
185+
raise ValueError('No game masters provided.')
186+
game_master = game_masters[0]
187+
188+
executor = self.get_executor()
189+
190+
if premise:
191+
# run observe on all game masters in parallel using concurrency
192+
tasks = {}
193+
for entity in game_masters:
194+
tasks[entity.name] = functools.partial(entity.observe, premise)
195+
concurrency.run_tasks(tasks, executor=executor)
196+
197+
for step in range(max_steps):
198+
if verbose:
199+
print(f'Step {step}')
200+
201+
if self.terminate(game_master, verbose):
202+
return
203+
204+
# run observe on all entities in parallel using concurrency
205+
tasks = {}
206+
for entity in entities:
207+
tasks[entity.name] = functools.partial(
208+
entity.observe, self.make_observation(game_master, entity, verbose)
209+
)
210+
concurrency.run_tasks(tasks, executor=executor)
211+
212+
next_entities = self.next_acting(game_master, entities)
213+
214+
if not next_entities:
215+
if verbose:
216+
print(termcolor.colored('No entities to act.', _PRINT_COLOR))
217+
return
218+
219+
player_qid_spec_list = self.next_action_spec(game_master, next_entities)
220+
221+
entity_map = {e.name: e for e in next_entities}
222+
entity_answers = {name: {} for name in entity_map.keys()}
223+
224+
for player_name, q_id, spec_str in player_qid_spec_list:
225+
if player_name not in entity_map:
226+
continue
227+
228+
entity = entity_map[player_name]
229+
agent = cast(entity_agent.EntityAgent, entity)
230+
231+
# We give the question and action spec and options as observation to
232+
# the entity agent and we later give only the action spec and
233+
# options for it to act.
234+
235+
observation = spec_str.replace('prompt: ', '')
236+
agent.observe(observation)
237+
238+
formatted_spec_str = spec_str.replace('{player_name}', player_name)
239+
action_spec = engine_lib.action_spec_parser(formatted_spec_str)
240+
answer = agent.act(action_spec)
241+
entity_answers[player_name][q_id] = answer
242+
243+
# Feed back answers to GM
244+
for player_name, qid_answer_map in entity_answers.items():
245+
for q_id, answer in qid_answer_map.items():
246+
observation = f'{PUTATIVE_EVENT_TAG} {player_name}: {q_id}: {answer}'
247+
game_master.observe(observation)
248+
249+
if verbose:
250+
print(termcolor.colored('Questionnaire round finished.', _PRINT_COLOR))
251+
252+
self.shutdown()
253+
254+
@override
255+
def resolve(
256+
self, game_master: entity_lib.Entity, putative_event: str
257+
) -> None:
258+
raise NotImplementedError
259+
260+
def shutdown(self, wait: bool = True) -> None:
261+
"""Shuts down any internal resources, like executors."""
262+
if hasattr(self, '_executor') and self._executor is not None:
263+
self._executor.shutdown(wait=wait)
264+
self._executor = None
265+
266+
@override
267+
def next_game_master(
268+
self,
269+
game_master: entity_lib.Entity,
270+
game_masters: Sequence[entity_lib.Entity],
271+
verbose: bool = False,
272+
) -> entity_lib.Entity:
273+
"""Select which game master to use for the next step."""
274+
if len(game_masters) == 1:
275+
return game_masters[0]
276+
game_masters_by_name = {
277+
game_master_.name: game_master_ for game_master_ in game_masters
278+
}
279+
next_game_master_name = game_master.act(
280+
action_spec=entity_lib.ActionSpec(
281+
call_to_action=self._call_to_next_game_master,
282+
output_type=entity_lib.OutputType.NEXT_GAME_MASTER,
283+
options=tuple(game_masters_by_name.keys()),
284+
)
285+
)
286+
if verbose:
287+
print(
288+
termcolor.colored(
289+
f'Game master: {next_game_master_name}', _PRINT_COLOR
290+
)
291+
)
292+
if next_game_master_name not in game_masters_by_name:
293+
raise ValueError(
294+
f'Selected game master "{next_game_master_name}" not found in:'
295+
f' {game_masters_by_name.keys()}'
296+
)
297+
return game_masters_by_name[next_game_master_name]

concordia/prefabs/simulation/questionnaire_simulation.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from concordia.associative_memory import basic_associative_memory as associative_memory
2626
from concordia.environment.engines import parallel_questionnaire
27+
from concordia.environment.engines import sequential_questionnaire
2728
from concordia.language_model import language_model
2829
from concordia.typing import entity as entity_lib
2930
from concordia.typing import entity_component
@@ -46,7 +47,11 @@ def __init__(
4647
config: Config,
4748
model: language_model.LanguageModel,
4849
embedder: Callable[[str], np.ndarray],
49-
engine: parallel_questionnaire.ParallelQuestionnaireEngine | None = None,
50+
engine: (
51+
parallel_questionnaire.ParallelQuestionnaireEngine
52+
| sequential_questionnaire.SequentialQuestionnaireEngine
53+
| None
54+
) = None,
5055
max_workers: int | None = None,
5156
verbose: bool = False,
5257
):

0 commit comments

Comments
 (0)