diff --git a/docs/simulation_competitions.md b/docs/simulation_competitions.md new file mode 100644 index 00000000..922f9e34 --- /dev/null +++ b/docs/simulation_competitions.md @@ -0,0 +1,145 @@ +# Tutorial: Simulation Competitions + +This tutorial walks you through interacting with a Kaggle simulation competition using the CLI — from finding the competition to downloading episode replays and agent logs. + +Simulation competitions (e.g., [Connect X](https://www.kaggle.com/competitions/connectx), [Lux AI](https://www.kaggle.com/competitions/lux-ai-season-3)) differ from standard competitions. Instead of submitting a CSV of predictions, you submit an agent (code) that plays against other agents in episodes. Each episode contains multiple agents competing against each other. You can identify simulation competitions on the [competitions page](https://www.kaggle.com/competitions) by their "Simulation" tag, or by looking for competitions that mention agents, bots, or game environments in their description. + +## 1. Find and Inspect the Competition + +Search for simulation competitions by keyword: + +```bash +kaggle competitions list -s simulation +``` + +Once you've identified a competition (e.g., `connectx`), view its pages to read the rules, evaluation criteria, and other details: + +```bash +kaggle competitions pages connectx +``` + +This lists the available pages (e.g., `description`, `rules`, `evaluation`, `data-description`). To read the full content of a page: + +```bash +kaggle competitions pages connectx --content +``` + +## 2. Accept the Competition Rules + +Before you can submit or download data, you **must** accept the competition rules on the Kaggle website. Navigate to the competition page (e.g., `https://www.kaggle.com/competitions/connectx`) and click "Join Competition" or "I Understand and Accept". + +You can verify you've joined by checking your entered competitions: + +```bash +kaggle competitions list --group entered +``` + +## 3. Download Competition Data + +Download the competition's starter kit and any provided data: + +```bash +kaggle competitions download connectx -p connectx-data +``` + +## 4. Submit Your Agent + +Simulation competitions require you to submit agent code. You can upload files directly from your local machine. + +**Single file agent** — if your agent is a single `main.py`: + +```bash +kaggle competitions submit connectx -f main.py -m "Single file agent v1" +``` + +**Multi-file agent** — if your agent spans multiple files, bundle them into a `submission.tar.gz` with `main.py` at the root: + +```bash +tar -czf submission.tar.gz main.py helper.py model_weights.pkl +kaggle competitions submit connectx -f submission.tar.gz -m "Multi-file agent v1" +``` + +**Notebook submission** — alternatively, you can submit via an existing Kaggle notebook: + +```bash +kaggle competitions submit connectx -k YOUR_USERNAME/connectx-agent -f submission.tar.gz -v 1 -m "Notebook agent v1" +``` + +## 5. Monitor Your Submission + +Check the status of your submissions: + +```bash +kaggle competitions submissions connectx +``` + +Note the submission ID from the output — you'll need it to view episodes. + +## 6. List Episodes for a Submission + +Once your submission has played some games, list the episodes: + +```bash +kaggle competitions episodes 12345678 +``` + +Replace `12345678` with your submission ID. This shows a table of episodes with columns: `id`, `createTime`, `endTime`, `state`, and `type`. + +To get the output in CSV format for scripting: + +```bash +kaggle competitions episodes 12345678 -v +``` + +## 7. Download an Episode Replay + +To download the replay data for a specific episode (useful for visualizing what happened): + +```bash +kaggle competitions replay 98765432 +``` + +This downloads the replay JSON to your current directory as `episode-98765432-replay.json`. To specify a download location: + +```bash +kaggle competitions replay 98765432 -p ./replays +``` + +## 8. Download Agent Logs + +To debug your agent's behavior, download the logs for a specific agent in an episode. You need the episode ID and the agent's index (0-based): + +```bash +# Download logs for the first agent (index 0) +kaggle competitions logs 98765432 0 + +# Download logs for the second agent (index 1) +kaggle competitions logs 98765432 1 -p ./logs +``` + +This downloads the log file as `episode-98765432-agent-0-logs.json`. + +## Putting It All Together + +Here's a typical workflow for iterating on a simulation competition agent: + +```bash +# Download competition data +kaggle competitions download connectx -p connectx-data + +# Submit your agent (single file) +kaggle competitions submit connectx -f main.py -m "v1" + +# Check submission status +kaggle competitions submissions connectx + +# List episodes (replace with your submission ID) +kaggle competitions episodes 12345678 + +# Download replay and logs for an episode +kaggle competitions replay 98765432 +kaggle competitions logs 98765432 0 + +# Check the leaderboard +kaggle competitions leaderboard connectx -s +``` diff --git a/src/kaggle/api/kaggle_api_extended.py b/src/kaggle/api/kaggle_api_extended.py index 78124f86..558b9e2a 100644 --- a/src/kaggle/api/kaggle_api_extended.py +++ b/src/kaggle/api/kaggle_api_extended.py @@ -84,6 +84,12 @@ ApiDataFile, ApiCreateCodeSubmissionResponse, ApiListCompetitionsResponse, + ApiListSubmissionEpisodesRequest, + ApiListSubmissionEpisodesResponse, + ApiGetEpisodeReplayRequest, + ApiGetEpisodeAgentLogsRequest, + ApiListCompetitionPagesRequest, + ApiListCompetitionPagesResponse, ) from kagglesdk.competitions.types.competition_enums import ( CompetitionListTab, @@ -624,6 +630,9 @@ class KaggleApi: model_instance_labels = ["version", "notes", "created", "size"] model_instance_version_fields = ["versionNumber", "variationSlug", "modelTitle", "isPrivate"] model_instance_version_labels = ["version", "variation", "title", "private"] + episode_fields = ["id", "createTime", "endTime", "state", "type"] + episode_agent_fields = ["submissionId", "index", "reward", "state", "teamName", "teamId"] + competition_page_fields = ["name"] def __init__(self, enable_oauth: bool = False): self.enable_oauth = enable_oauth @@ -1742,6 +1751,161 @@ def competition_leaderboard_cli( else: print("No results found") + def competition_list_episodes(self, submission_id: int): + """List episodes for a submission in a simulation competition. + + Args: + submission_id (int): The submission ID to list episodes for. + + Returns: + list: A list of ApiEpisode objects. + """ + with self.build_kaggle_client() as kaggle: + request = ApiListSubmissionEpisodesRequest() + request.submission_id = submission_id + response = kaggle.competitions.competition_api_client.list_submission_episodes(request) + return response.episodes + + def competition_list_episodes_cli(self, submission_id, csv_display=False, quiet=False): + """CLI wrapper for competition_list_episodes. + + Args: + submission_id (int): The submission ID. + csv_display (bool): If True, print CSV instead of table. + quiet (bool): Suppress verbose output. + """ + episodes = self.competition_list_episodes(submission_id) + if episodes: + if csv_display: + self.print_csv(episodes, self.episode_fields) + else: + self.print_table(episodes, self.episode_fields) + if not quiet: + print( + '\nUse "kaggle competitions replay " to download a replay, ' + 'or "kaggle competitions logs " for agent logs.' + ) + else: + print("No episodes found") + + def competition_episode_replay(self, episode_id: int, path: Optional[str] = None, quiet: bool = True): + """Download the replay for an episode. + + Args: + episode_id (int): The episode ID. + path (Optional[str]): A path to download the file to. + quiet (bool): Suppress verbose output. + """ + with self.build_kaggle_client() as kaggle: + request = ApiGetEpisodeReplayRequest() + request.episode_id = episode_id + response = kaggle.competitions.competition_api_client.get_episode_replay(request) + if path is None: + effective_path = os.getcwd() + else: + effective_path = path + outfile = os.path.join(effective_path, f"episode-{episode_id}-replay.json") + self.download_file(response, outfile, kaggle.http_client(), quiet) + if not quiet: + print(f"Replay downloaded to: {outfile}") + + def competition_episode_replay_cli(self, episode_id, path=None, quiet=False): + """CLI wrapper for competition_episode_replay. + + Args: + episode_id (int): The episode ID. + path (Optional[str]): A path to download the file to. + quiet (bool): Suppress verbose output. + """ + self.competition_episode_replay(episode_id, path, quiet) + + def competition_episode_agent_logs( + self, episode_id: int, agent_index: int, path: Optional[str] = None, quiet: bool = True + ): + """Download logs for a specific agent in an episode. + + Args: + episode_id (int): The episode ID. + agent_index (int): The agent index. + path (Optional[str]): A path to download the file to. + quiet (bool): Suppress verbose output. + """ + with self.build_kaggle_client() as kaggle: + request = ApiGetEpisodeAgentLogsRequest() + request.episode_id = episode_id + request.agent_index = agent_index + response = kaggle.competitions.competition_api_client.get_episode_agent_logs(request) + if path is None: + effective_path = os.getcwd() + else: + effective_path = path + outfile = os.path.join(effective_path, f"episode-{episode_id}-agent-{agent_index}-logs.json") + self.download_file(response, outfile, kaggle.http_client(), quiet) + if not quiet: + print(f"Agent logs downloaded to: {outfile}") + + def competition_episode_agent_logs_cli(self, episode_id, agent_index, path=None, quiet=False): + """CLI wrapper for competition_episode_agent_logs. + + Args: + episode_id (int): The episode ID. + agent_index (int): The agent index. + path (Optional[str]): A path to download the file to. + quiet (bool): Suppress verbose output. + """ + self.competition_episode_agent_logs(episode_id, agent_index, path, quiet) + + def competition_list_pages(self, competition: str, page_name: Optional[str] = None): + """List pages for a competition. + + Args: + competition (str): The competition name. + page_name (Optional[str]): Filter to a specific page by name. + + Returns: + list: A list of ApiCompetitionPage objects. + """ + with self.build_kaggle_client() as kaggle: + request = ApiListCompetitionPagesRequest() + request.competition_name = competition + if page_name: + request.page_name = page_name + response = kaggle.competitions.competition_api_client.list_competition_pages(request) + return response.pages + + def competition_list_pages_cli( + self, competition=None, competition_opt=None, csv_display=False, quiet=False, content=False, + page_name=None + ): + """CLI wrapper for competition_list_pages. + + Args: + competition: The competition name. + competition_opt: An alternative competition option provided by cli. + csv_display (bool): If True, print CSV instead of table. + quiet (bool): Suppress verbose output. + content (bool): If True, show full page content. + page_name (Optional[str]): Filter to a specific page by name. + """ + competition = competition or competition_opt + if competition is None: + competition = self.get_config_value(self.CONFIG_NAME_COMPETITION) + if competition is not None and not quiet: + print("Using competition: " + competition) + + if competition is None: + raise ValueError("No competition specified") + + pages = self.competition_list_pages(competition, page_name=page_name) + if pages: + fields = ["name", "content"] if content else self.competition_page_fields + if csv_display: + self.print_csv(pages, fields) + else: + self.print_table(pages, fields) + else: + print("No pages found") + def dataset_list( self, sort_by: Optional[str] = None, diff --git a/src/kaggle/cli.py b/src/kaggle/cli.py index 5a4567c0..cda2f870 100644 --- a/src/kaggle/cli.py +++ b/src/kaggle/cli.py @@ -276,6 +276,89 @@ def parse_competitions(subparsers) -> None: parser_competitions_leaderboard._action_groups.append(parser_competitions_leaderboard_optional) parser_competitions_leaderboard.set_defaults(func=api.competition_leaderboard_cli) + # Competitions list episodes + parser_competitions_episodes = subparsers_competitions.add_parser( + "episodes", formatter_class=argparse.RawTextHelpFormatter, help=Help.command_competitions_episodes + ) + parser_competitions_episodes_optional = parser_competitions_episodes._action_groups.pop() + parser_competitions_episodes_optional.add_argument( + "submission_id", type=int, + help='Submission ID (find yours with "kaggle competitions submissions ")' + ) + parser_competitions_episodes_optional.add_argument( + "-v", "--csv", dest="csv_display", action="store_true", help=Help.param_csv + ) + parser_competitions_episodes_optional.add_argument( + "-q", "--quiet", dest="quiet", action="store_true", help=Help.param_quiet + ) + parser_competitions_episodes._action_groups.append(parser_competitions_episodes_optional) + parser_competitions_episodes.set_defaults(func=api.competition_list_episodes_cli) + + # Competitions episode replay + parser_competitions_episode_replay = subparsers_competitions.add_parser( + "replay", formatter_class=argparse.RawTextHelpFormatter, help=Help.command_competitions_episode_replay + ) + parser_competitions_episode_replay_optional = parser_competitions_episode_replay._action_groups.pop() + parser_competitions_episode_replay_optional.add_argument( + "episode_id", type=int, + help='Episode ID (find these with "kaggle competitions episodes ")' + ) + parser_competitions_episode_replay_optional.add_argument( + "-p", "--path", dest="path", required=False, help=Help.param_downfolder + ) + parser_competitions_episode_replay_optional.add_argument( + "-q", "--quiet", dest="quiet", action="store_true", help=Help.param_quiet + ) + parser_competitions_episode_replay._action_groups.append(parser_competitions_episode_replay_optional) + parser_competitions_episode_replay.set_defaults(func=api.competition_episode_replay_cli) + + # Competitions episode agent logs + parser_competitions_episode_logs = subparsers_competitions.add_parser( + "logs", formatter_class=argparse.RawTextHelpFormatter, help=Help.command_competitions_episode_logs + ) + parser_competitions_episode_logs_optional = parser_competitions_episode_logs._action_groups.pop() + parser_competitions_episode_logs_optional.add_argument( + "episode_id", type=int, + help='Episode ID (find these with "kaggle competitions episodes ")' + ) + parser_competitions_episode_logs_optional.add_argument( + "agent_index", type=int, + help="Agent index (0-based position of the agent in the episode)" + ) + parser_competitions_episode_logs_optional.add_argument( + "-p", "--path", dest="path", required=False, help=Help.param_downfolder + ) + parser_competitions_episode_logs_optional.add_argument( + "-q", "--quiet", dest="quiet", action="store_true", help=Help.param_quiet + ) + parser_competitions_episode_logs._action_groups.append(parser_competitions_episode_logs_optional) + parser_competitions_episode_logs.set_defaults(func=api.competition_episode_agent_logs_cli) + + # Competitions list pages + parser_competitions_pages = subparsers_competitions.add_parser( + "pages", formatter_class=argparse.RawTextHelpFormatter, help=Help.command_competitions_pages + ) + parser_competitions_pages_optional = parser_competitions_pages._action_groups.pop() + parser_competitions_pages_optional.add_argument("competition", nargs="?", default=None, help=Help.param_competition) + parser_competitions_pages_optional.add_argument( + "-c", "--competition", dest="competition_opt", required=False, help=argparse.SUPPRESS + ) + parser_competitions_pages_optional.add_argument( + "-v", "--csv", dest="csv_display", action="store_true", help=Help.param_csv + ) + parser_competitions_pages_optional.add_argument( + "-q", "--quiet", dest="quiet", action="store_true", help=Help.param_quiet + ) + parser_competitions_pages_optional.add_argument( + "--content", dest="content", action="store_true", help="Show full page content" + ) + parser_competitions_pages_optional.add_argument( + "--page-name", dest="page_name", required=False, + help='Filter to a specific page (e.g. "description", "rules", "evaluation")' + ) + parser_competitions_pages._action_groups.append(parser_competitions_pages_optional) + parser_competitions_pages.set_defaults(func=api.competition_list_pages_cli) + def parse_datasets(subparsers) -> None: parser_datasets = subparsers.add_parser( @@ -1116,7 +1199,10 @@ class Help(object): "config", "auth", ] - competitions_choices = ["list", "files", "download", "submit", "submissions", "leaderboard"] + competitions_choices = [ + "list", "files", "download", "submit", "submissions", "leaderboard", + "episodes", "replay", "logs", "pages", + ] datasets_choices = ["list", "files", "download", "create", "version", "init", "metadata", "status", "delete"] kernels_choices = ["list", "files", "get", "init", "push", "pull", "output", "status", "update", "delete"] models_choices = ["instances", "i", "variations", "v", "get", "list", "init", "create", "delete", "update"] @@ -1169,6 +1255,10 @@ class Help(object): command_competitions_submit = "Make a new competition submission" command_competitions_submissions = "Show your competition submissions" command_competitions_leaderboard = "Get competition leaderboard information" + command_competitions_episodes = "List episodes for a submission in a simulation competition" + command_competitions_episode_replay = "Download the replay for a simulation episode" + command_competitions_episode_logs = "Download agent logs for a simulation episode" + command_competitions_pages = "List pages for a competition" # Datasets commands command_datasets_list = "List available datasets" diff --git a/tests/test_commands.sh b/tests/test_commands.sh index f15a1ebe..39f7ce89 100755 --- a/tests/test_commands.sh +++ b/tests/test_commands.sh @@ -24,6 +24,10 @@ kaggle c submissions house-prices-advanced-regression-techniques -v -q echo "kaggle competitions leaderboard" kaggle c leaderboard titanic -v -q -d -p leaders kaggle c leaderboard titanic -s > leaderboard.txt +echo "kaggle competitions pages" +kaggle c pages titanic +kaggle c pages titanic -v -q +kaggle c pages titanic --page-name rules --content rm -r titanic.zip tost sample_submission.csv leaders leaderboard.txt echo "kaggle kernels list"