Skip to content

Commit a660496

Browse files
authored
Add prompt section to game configs (#26)
* Add `GameContext` * Prompt rendering working * Add prompts for remaining games * Fix test * Get rid of games/utils.py; copy traj to environment * Respond to feedback
1 parent 67c9441 commit a660496

16 files changed

Lines changed: 276 additions & 160 deletions

codeclash/agents/__init__.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from codeclash.agents.abstract import Player
22
from codeclash.agents.dummy import Dummy
33
from codeclash.agents.minisweagent import MiniSWEAgent
4+
from codeclash.agents.utils import GameContext
5+
from codeclash.constants import DIR_WORK
46
from codeclash.games.abstract import CodeGame
57

68

7-
def get_agent(config: dict, game: CodeGame) -> Player:
9+
def get_agent(config: dict, prompts: dict, game: CodeGame) -> Player:
810
agents = {
911
"dummy": Dummy,
1012
"mini": MiniSWEAgent,
@@ -14,12 +16,16 @@ def get_agent(config: dict, game: CodeGame) -> Player:
1416
environment = game.get_environment(
1517
f"{game.game_id}.{config['name']}"
1618
) # NOTE: MUST be branch_name (defined in agents/abstract.py)
17-
template_vars = {
18-
"game_name": game.name,
19-
"game_id": game.game_id,
20-
"rounds": game.rounds,
21-
"round": 1,
22-
"player_id": config["name"],
23-
"game_description": game.config.get("description", ""),
24-
}
25-
return agents(config, environment, template_vars)
19+
return agents(
20+
config,
21+
environment,
22+
GameContext(
23+
id=game.game_id,
24+
name=game.name,
25+
player_id=config["name"],
26+
prompts=prompts,
27+
round=1,
28+
rounds=game.rounds,
29+
working_dir=str(DIR_WORK),
30+
),
31+
)

codeclash/agents/abstract.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from dotenv import load_dotenv
55
from minisweagent import Environment
66

7+
from codeclash.agents.utils import GameContext
78
from codeclash.constants import DIR_LOGS, GH_ORG
89
from codeclash.utils.environment import assert_zero_exit_code
910
from codeclash.utils.log import get_logger
@@ -16,39 +17,38 @@ def __init__(
1617
self,
1718
config: dict,
1819
environment: Environment,
19-
template_vars: dict,
20+
game_context: GameContext,
2021
):
2122
self.config = config
2223
self.name = config["name"]
2324
self.environment = environment
24-
self.template_vars = template_vars
25+
self.game_context = game_context
26+
self.game_context.render_and_set_prompts()
2527
self.logger = get_logger(
2628
self.name,
27-
log_path=DIR_LOGS / template_vars["game_id"] / f"{self.name}.log",
29+
log_path=DIR_LOGS / game_context.id / f"{self.name}.log",
2830
emoji="👤",
2931
)
3032

3133
@property
3234
def branch_name(self):
3335
"""Get the branch name for the agent's codebase."""
34-
return f"{self.template_vars['game_id']}.{self.name}"
36+
return f"{self.game_context.id}.{self.name}"
3537

3638
def commit(self):
3739
"""Commit changes to the agent's codebase."""
38-
rounds = self.template_vars["rounds"]
40+
r, rounds = self.game_context.round, self.game_context.rounds
3941
for cmd in [
4042
"git add -A",
41-
f"git commit --allow-empty -m 'Round {self.round}/{rounds} Update'",
43+
f"git commit --allow-empty -m 'Round {r}/{rounds} Update'",
4244
]:
4345
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
44-
self.logger.info(
45-
f"Committed changes for {self.name} for round {self.round}/{rounds}"
46-
)
46+
self.logger.info(f"Committed changes for {self.name} for round {r}/{rounds}")
4747

4848
def on_round_update(self, new_round: int):
4949
"""Update the agent's round to match the game round."""
50-
self.round = new_round
51-
self.template_vars["round"] = new_round
50+
self.game_context.round = new_round
51+
self.game_context.render_and_set_prompts()
5252

5353
def push(self):
5454
"""Push codebase to a branch on the game's remote repository."""
@@ -58,7 +58,7 @@ def push(self):
5858

5959
for cmd in [
6060
"git remote remove origin",
61-
f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.template_vars['game_name']}.git",
61+
f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.game_context.name}.git",
6262
f"git push origin {self.branch_name}",
6363
]:
6464
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)

codeclash/agents/minisweagent.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
from rich.console import Console
1616

1717
from codeclash.agents.abstract import Player
18-
from codeclash.agents.utils import resolve_api_key
18+
from codeclash.agents.utils import GameContext, resolve_api_key
1919
from codeclash.constants import DIR_LOGS
20+
from codeclash.utils.environment import copy_file_to_container
2021

2122

2223
class ClashAgent(DefaultAgent):
@@ -30,15 +31,15 @@ def __init__(
3031
model: Model,
3132
env: Environment,
3233
name: str,
33-
template_vars: dict,
34+
game_context: GameContext,
3435
*,
3536
logger: logging.Logger,
3637
config_class: Callable = AgentConfig,
3738
**kwargs,
3839
):
3940
super().__init__(model, env, config_class=config_class, **kwargs)
4041
self.name = name
41-
self.template_vars = template_vars
42+
self.game_context = game_context
4243
self.console = Console()
4344
self.logger = logger
4445

@@ -56,7 +57,7 @@ def render_template(self, template: str, **kwargs) -> str:
5657
| asdict(self.env.config)
5758
| asdict(self.model.config)
5859
| platform.uname()._asdict()
59-
| self.template_vars
60+
| self.game_context.to_dict()
6061
)
6162
return Template(template).render(**kwargs, **cs, **os.environ)
6263

@@ -68,16 +69,18 @@ def run(self) -> tuple[str, str]:
6869
class MiniSWEAgent(Player):
6970
"""Player with agentic code editing capabilities"""
7071

71-
def __init__(self, config: dict, environment: Environment, template_vars: dict):
72-
super().__init__(config, environment=environment, template_vars=template_vars)
72+
def __init__(
73+
self, config: dict, environment: Environment, game_context: GameContext
74+
):
75+
super().__init__(config, environment=environment, game_context=game_context)
7376
self.agent = ClashAgent(
7477
LitellmModel(
7578
model_name=config["model"],
7679
model_kwargs={"api_key": resolve_api_key(config["model"])},
7780
),
7881
self.environment,
7982
self.name,
80-
template_vars,
83+
game_context,
8184
logger=self.logger,
8285
**yaml.safe_load(Path(config["config"]).read_text())["agent"],
8386
)
@@ -93,11 +96,16 @@ def run(self):
9396
result = exc_message
9497
print(exc_message)
9598
finally:
99+
traj_path = (
100+
DIR_LOGS
101+
/ self.game_context.id
102+
/ f"{self.name}_r{self.game_context.round}.traj.json"
103+
)
96104
save_traj(
97105
self.agent, # type: ignore
98-
DIR_LOGS
99-
/ f"{self.template_vars['game_id']}/{self.name}_r{self.template_vars['round']}.traj.json",
106+
traj_path,
100107
exit_status=exit_status,
101108
result=result,
102109
)
110+
copy_file_to_container(self.environment, traj_path, traj_path)
103111
self.commit()

codeclash/agents/utils.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
2+
from dataclasses import asdict, dataclass
23

34
from dotenv import load_dotenv
5+
from jinja2 import Template
46

57
load_dotenv()
68

@@ -10,3 +12,45 @@ def resolve_api_key(model: str) -> str:
1012
return os.getenv("ANTHROPIC_API_KEY")
1113
if "gpt" in model:
1214
return os.getenv("OPENAI_API_KEY")
15+
16+
17+
@dataclass
18+
class GameContext:
19+
"""
20+
A class that gives agent access to a partial view of the game state.
21+
22+
NOTE: Instead of passing `game` directly as a reference to the agent,
23+
we create this interface instead to make the communication of game state
24+
more explicit and controlled. We go with this loose coupling to avoid
25+
making the agent too dependent on the entire game object.
26+
"""
27+
28+
id: str
29+
name: str
30+
player_id: str
31+
prompts: dict
32+
round: int
33+
rounds: int
34+
working_dir: str
35+
36+
def render_and_set_prompts(self):
37+
"""Render and set prompts using the current game context."""
38+
context = asdict(self)
39+
del context["prompts"]
40+
for key, template_str in self.prompts.items():
41+
rendered = Template(template_str).render(**context)
42+
setattr(self, key, rendered)
43+
44+
def to_dict(self):
45+
"""Convert the GameContext to a dictionary, including dynamically added attributes."""
46+
result = asdict(self)
47+
declared = set(self.__dataclass_fields__)
48+
for attr in dir(self):
49+
if (
50+
not attr.startswith("_")
51+
and attr not in declared
52+
and not callable(getattr(self, attr))
53+
):
54+
result[attr] = getattr(self, attr)
55+
del result["prompts"]
56+
return result

codeclash/games/abstract.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
from codeclash.agents.abstract import Player
1313
from codeclash.constants import DIR_LOGS, DIR_WORK, GH_ORG
14-
from codeclash.games.utils import copy_between_containers, copy_file_from_container
15-
from codeclash.utils.environment import assert_zero_exit_code
14+
from codeclash.utils.environment import (
15+
assert_zero_exit_code,
16+
copy_between_containers,
17+
copy_file_from_container,
18+
)
1619
from codeclash.utils.log import get_logger
1720

1821

codeclash/games/robocode/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from codeclash.agents.abstract import Player
44
from codeclash.games.abstract import CodeGame
5-
from codeclash.games.utils import copy_file_to_container
5+
from codeclash.utils.environment import copy_file_to_container
66

77

88
class RoboCodeGame(CodeGame):

codeclash/games/utils.py

Lines changed: 0 additions & 101 deletions
This file was deleted.

0 commit comments

Comments
 (0)