11import os
2+ import uuid
23from abc import ABC , abstractmethod
34
45from dotenv import load_dotenv
5- from minisweagent import Environment
6+ from minisweagent . environments . docker import DockerEnvironment
67
78from codeclash .agents .utils import GameContext
89from codeclash .constants import GH_ORG
9- from codeclash .utils .environment import assert_zero_exit_code
10+ from codeclash .tournaments .utils .git_utils import filter_git_diff
11+ from codeclash .utils .environment import assert_zero_exit_code , create_file_on_container
1012from codeclash .utils .log import get_logger
1113
1214load_dotenv ()
@@ -16,41 +18,52 @@ class Player(ABC):
1618 def __init__ (
1719 self ,
1820 config : dict ,
19- environment : Environment ,
21+ environment : DockerEnvironment ,
2022 game_context : GameContext ,
21- ):
23+ ) -> None :
2224 self .config = config
2325 self .name = config ["name" ]
26+ self ._player_unique_id = uuid .uuid4 ()
27+ """Unique ID that doesn't clash even accross multiple games. Used for git tags."""
2428 self .environment = environment
2529 self .game_context = game_context
26- self .game_context .render_and_set_prompts ()
2730 self .logger = get_logger (
2831 self .name ,
2932 log_path = self .game_context .log_local / f"{ self .name } .log" ,
3033 emoji = "👤" ,
3134 )
35+ self ._metadata = {
36+ "name" : self .name ,
37+ "player_unique_id" : self ._player_unique_id ,
38+ "diff" : {0 : "" }, # mapping round -> diff
39+ "incremental_diff" : {0 : "" }, # mapping round -> diff
40+ }
3241
33- @property
34- def branch_name (self ):
35- """Get the branch name for the agent's codebase."""
36- return f"{ self .game_context .id } .{ self .name } "
37-
38- def commit (self ):
39- """Commit changes to the agent's codebase."""
40- r , rounds = self .game_context .round , self .game_context .rounds
41- for cmd in [
42- "git add -A" ,
43- f"git commit --allow-empty -m 'Round { r } /{ rounds } Update'" ,
44- ]:
45- assert_zero_exit_code (self .environment .execute (cmd ), logger = self .logger )
46- self .logger .info (f"Committed changes for { self .name } for round { r } /{ rounds } " )
42+ # --- Main methods ---
4743
48- def on_round_update (self , new_round : int ):
49- """Update the agent's round to match the game round."""
44+ def pre_run_hook (self , * , new_round : int ) -> None :
45+ """Should be called before we call the run method."""
46+ if new_round == 1 :
47+ self ._tag_round (0 )
5048 self .game_context .round = new_round
51- self .game_context .render_and_set_prompts ()
5249
53- def push (self ):
50+ def post_run_hook (self , * , round : int ) -> None :
51+ """Should be called after we called the run method."""
52+ self ._commit ()
53+ self ._metadata ["diff" ][round ] = self ._get_round_diff (round )
54+ self ._metadata ["incremental_diff" ][round ] = self ._get_round_diff (
55+ round , incremental = True
56+ )
57+
58+ @abstractmethod
59+ def run (self ) -> None :
60+ """Given the observation / recap, update the codebase"""
61+
62+ def get_metadata (self ) -> dict :
63+ """Get metadata for the agent."""
64+ return self ._metadata
65+
66+ def push (self ) -> None :
5467 """Push codebase to a branch on the game's remote repository."""
5568 token = os .getenv ("GITHUB_TOKEN" )
5669 if not token :
@@ -59,13 +72,98 @@ def push(self):
5972 for cmd in [
6073 "git remote remove origin" ,
6174 f"git remote add origin https://x-access-token:{ token } @github.com/{ GH_ORG } /{ self .game_context .name } .git" ,
62- f"git push origin { self .branch_name } " ,
75+ f"git push origin { self ._branch_name } " ,
76+ "git push origin --tags" ,
6377 ]:
6478 assert_zero_exit_code (self .environment .execute (cmd ), logger = self .logger )
6579 self .logger .info (
66- f"Pushed { self .name } commit history to remote repository (branch { self .branch_name } )"
80+ f"Pushed { self .name } commit history to remote repository (branch { self ._branch_name } )"
6781 )
6882
69- @abstractmethod
70- def run (self ):
71- """Given the observation / recap, update the codebase"""
83+ def reset_and_apply_patch (
84+ self , patch : str , * , base_commit : str = "" , filter_patch : bool = True
85+ ) -> None :
86+ """Clean all uncommited changes. If base_commit is provided, reset to that commit.
87+ Then apply the patch to the codebase.
88+ """
89+ # Need to clean before we copy over the patch (else it's gonna be removed by git clean)
90+ self .logger .debug (
91+ assert_zero_exit_code (
92+ self .environment .execute (
93+ f"git reset --hard { base_commit } && git clean -fd"
94+ )
95+ )
96+ )
97+
98+ patch = filter_git_diff (patch ) if filter_patch else patch
99+
100+ if not patch .strip ():
101+ self .logger .debug ("No patch to apply, skipping" )
102+ return
103+
104+ create_file_on_container (
105+ container = self .environment , # type: ignore
106+ content = patch ,
107+ dest_path = "tmp_patch.txt" ,
108+ )
109+
110+ self .logger .debug (f"Applying patch to agent's codebase: { patch } " )
111+
112+ commands = ["git status" , "git apply tmp_patch.txt" , "rm -f tmp_patch.txt" ]
113+ for cmd in commands :
114+ self .logger .debug (f"Executing command: { cmd } " )
115+ out = assert_zero_exit_code (
116+ self .environment .execute (cmd ), logger = self .logger
117+ )
118+ self .logger .debug (out )
119+
120+ # --- Helper methods ---
121+
122+ def _tag_round (self , round : int ) -> None :
123+ """Git tag the codebase at the given round."""
124+ assert_zero_exit_code (
125+ self .environment .execute (
126+ f"git tag -a { self ._get_round_tag_name (round )} -m 'Round { round } Update'"
127+ ),
128+ logger = self .logger ,
129+ )
130+
131+ @property
132+ def _branch_name (self ) -> str :
133+ """Get the branch name for the agent's codebase."""
134+ return f"{ self .game_context .id } .{ self .name } "
135+
136+ def _get_round_tag_name (self , round : int ) -> str :
137+ """Get git tag name for the version of the codebase at the given round."""
138+ return f"{ self ._player_unique_id } -round-{ round } "
139+
140+ def _commit (self ) -> None :
141+ """Commit changes to the agent's codebase."""
142+ r = self .game_context .round
143+ for cmd in [
144+ "git add -A" ,
145+ f"git commit --allow-empty -m 'Round { r } Update'" ,
146+ ]:
147+ assert_zero_exit_code (self .environment .execute (cmd ), logger = self .logger )
148+ self ._tag_round (r )
149+ self .logger .info (f"Committed changes for { self .name } for round { r } " )
150+
151+ def _get_round_diff (self , round : int , * , incremental : bool = False ) -> str :
152+ """Get the diff between the round and initial version (round 0).
153+ If incremental is True, get the diff between the round and the previous round.
154+ Returns empty string if round is 0.
155+ """
156+ if round == 0 :
157+ return ""
158+ if incremental :
159+ previous_round_tag = self ._get_round_tag_name (round - 1 )
160+ else :
161+ previous_round_tag = self ._get_round_tag_name (0 )
162+ current_round_tag = self ._get_round_tag_name (round )
163+ out = assert_zero_exit_code (
164+ self .environment .execute (
165+ f"git diff { previous_round_tag } ..{ current_round_tag } "
166+ ),
167+ logger = self .logger ,
168+ )
169+ return out ["output" ]
0 commit comments