Skip to content

Commit ad6a1fe

Browse files
committed
Allow !include in yaml files
1 parent 9beb0fb commit ad6a1fe

11 files changed

Lines changed: 249 additions & 204 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ repos:
77
hooks:
88
- id: trailing-whitespace
99
- id: end-of-file-fixer
10-
- id: check-yaml
10+
# - id: check-yaml
1111
- id: check-added-large-files
1212
- id: check-merge-conflict
1313
- id: debug-statements

codeclash/agents/minisweagent.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@
44
import traceback
55
from collections.abc import Callable
66
from dataclasses import asdict
7-
from pathlib import Path
87

9-
import yaml
108
from jinja2 import Template
119
from minisweagent import Model
1210
from minisweagent.agents.default import AgentConfig, DefaultAgent
1311
from minisweagent.environments.docker import DockerEnvironment
14-
from minisweagent.models.litellm_model import LitellmModel
12+
from minisweagent.models import get_model
1513
from minisweagent.run.utils.save import save_traj
1614
from rich.console import Console
1715

1816
from codeclash.agents.abstract import Player
19-
from codeclash.agents.utils import GameContext, resolve_api_key
17+
from codeclash.agents.utils import GameContext
2018
from codeclash.utils.environment import copy_to_container
2119

2220

@@ -71,16 +69,14 @@ def __init__(self, config: dict, environment: DockerEnvironment, game_context: G
7169
super().__init__(config, environment=environment, game_context=game_context)
7270

7371
def run(self):
72+
model = get_model(config=self.config["config"]["model"])
7473
self.agent = ClashAgent(
75-
LitellmModel(
76-
model_name=self.config["model"],
77-
model_kwargs={"api_key": resolve_api_key(self.config["model"])},
78-
),
79-
self.environment,
80-
self.name,
81-
self.game_context,
74+
model=model,
75+
env=self.environment,
76+
name=self.name,
77+
game_context=self.game_context,
8278
logger=self.logger,
83-
**yaml.safe_load(Path(self.config["config"]).read_text())["agent"],
79+
**self.config["config"]["agent"],
8480
)
8581
exit_status = None
8682
result = None

codeclash/agents/utils.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import os
21
from pathlib import Path
32

43
from dotenv import load_dotenv
@@ -8,14 +7,6 @@
87
load_dotenv()
98

109

11-
def resolve_api_key(model: str) -> str:
12-
if "claude" in model:
13-
return os.getenv("ANTHROPIC_API_KEY")
14-
if "gpt" in model:
15-
return os.getenv("OPENAI_API_KEY")
16-
return ""
17-
18-
1910
class GameContext(BaseModel):
2011
"""
2112
A class that gives agent access to a partial view of the game state.

codeclash/tournaments/single_player.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from codeclash.agents.utils import GameContext
1111
from codeclash.constants import DIR_WORK
1212
from codeclash.games import get_game
13-
from codeclash.games.abstract import CodeGame
13+
from codeclash.games.abstract import CodeGame, RoundRecord
1414
from codeclash.tournaments.abstract import AbstractTournament
1515
from codeclash.tournaments.utils.git_utils import filter_git_diff
1616
from codeclash.utils.environment import copy_to_container
@@ -31,7 +31,7 @@ def __init__(self, config: dict, *, cleanup: bool = False):
3131
self.mirror_agent: Player = self.get_agent(mirror_agent_config, round=0)
3232

3333
@property
34-
def scoreboard(self) -> list[tuple[int, str]]:
34+
def scoreboard(self) -> list[tuple[int, RoundRecord]]:
3535
return self._metadata.setdefault("scoreboard", [])
3636

3737
@property
@@ -90,18 +90,19 @@ def run_training_round(self, round_num: int) -> None:
9090
record = self.game.run_round([self.agent, self.mirror_agent])
9191

9292
# Handle bookkeeping that was previously in the game
93-
self.scoreboard.append(record.stats)
93+
self.scoreboard.append((round_num, record))
9494
self.logger.info(f"Round {round_num}:\n{record.stats}")
9595

9696
# Write log to file
97-
for idx, lo in enumerate(record.logs):
97+
for idx, lo in enumerate(record.data.logs):
9898
round_log_path = self.game.log_local / f"round_{round_num}" / f"sim_{idx}.log"
99+
round_log_path.parent.mkdir(parents=True, exist_ok=True)
99100
round_log_path.write_text(lo)
100101

101102
# Copy log to main agent environment only
102103
self.logger.info(f"Copying round {round_num} log(s) to {self.agent.name}'s container...")
103104
copy_to_container(
104-
self.agent,
105+
self.agent.environment,
105106
self.game.log_local / f"round_{round_num}",
106107
f"logs/round_{round_num}/",
107108
)

codeclash/utils/yaml_utils.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import re
2+
from pathlib import Path
3+
4+
5+
def resolve_includes(yaml_content: str, *, base_dir: Path = Path(".")) -> str:
6+
"""Pre-process YAML content to resolve all !include directives.
7+
This will take the yaml content as a string and return the same string with all
8+
!include directives resolved.
9+
10+
The reason we don't go with pyyaml-include or similar options is that they don't
11+
support merge keys, see https://github.com/tanbro/pyyaml-include/issues/53 .
12+
"""
13+
14+
def include_replacer(match):
15+
indentation = match.group(1) # Capture the indentation
16+
prefix = match.group(2) # What comes before !include (like "<<: " or "key: ")
17+
include_path = match.group(3)
18+
full_path = base_dir / include_path
19+
20+
included_content = full_path.read_text().strip()
21+
22+
# Handle merge key specially
23+
if prefix.strip() == "<<:":
24+
# For merge keys, include the content with proper indentation
25+
if indentation:
26+
included_lines = included_content.splitlines()
27+
indented_lines = [indentation + line if line.strip() else line for line in included_lines]
28+
return indentation + "<<:\n" + "\n".join(indented_lines)
29+
else:
30+
# Indent the included content by 2 spaces for proper merge format
31+
included_lines = included_content.splitlines()
32+
indented_lines = [" " + line if line.strip() else line for line in included_lines]
33+
return "<<:\n" + "\n".join(indented_lines)
34+
else:
35+
# For regular includes, we need to handle indentation properly
36+
# If the prefix ends with ': ', we need to add a newline and indent the content
37+
if prefix.strip().endswith(":"):
38+
# Calculate the indentation for the included content
39+
content_indent = indentation + " " # Add 2 spaces for proper YAML nesting
40+
included_lines = included_content.splitlines()
41+
indented_lines = [content_indent + line if line.strip() else line for line in included_lines]
42+
return indentation + prefix.strip() + "\n" + "\n".join(indented_lines)
43+
else:
44+
# For cases where it's not a key assignment, just replace with content
45+
return indentation + prefix + included_content
46+
47+
# Pattern to match !include directives with proper capture groups
48+
# Group 1: indentation, Group 2: prefix (like "<<: " or "key: "), Group 3: file path
49+
include_pattern = r"^(\s*)(.*?)!include\s+(.+)$"
50+
51+
# Keep resolving includes until no more are found
52+
while re.search(include_pattern, yaml_content, re.MULTILINE):
53+
yaml_content = re.sub(include_pattern, include_replacer, yaml_content, flags=re.MULTILINE)
54+
55+
return yaml_content

configs/battlesnake.yaml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ game:
88
tournament:
99
rounds: 2
1010
players:
11-
- agent: mini
12-
name: p1
13-
config: configs/mini/default.yaml
14-
model: openai/gpt-5-mini
15-
- agent: dummy
16-
name: p2
11+
- agent: mini
12+
name: p1
13+
config:
14+
agent: !include mini/default.yaml
15+
model:
16+
model_name: openai/gpt-5-mini
17+
- agent: dummy
18+
name: p2
1719
prompts:
1820
game_description: |
1921
You are a software developer ({{player_id}}) competing in a coding game called BattleSnake.

configs/battlesnake_single_player.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ tournament:
1010
evaluate_matrix: true
1111
player:
1212
agent: mini
13-
config: configs/mini/default.yaml
14-
model: openai/gpt-5-mini
1513
name: main
14+
config:
15+
agent: !include mini/default.yaml
16+
model:
17+
model_name: openai/gpt-5-mini
1618
prompts:
1719
game_description: |
1820
You are a software developer ({{player_id}}) competing in a coding game called BattleSnake.

0 commit comments

Comments
 (0)