From 4f6490d2ad57b5e810c89e9ccc08555c271d3786 Mon Sep 17 00:00:00 2001 From: Carol Jung Date: Wed, 26 Feb 2025 14:14:19 -0800 Subject: [PATCH 1/4] feat: support --daemon flow in codegen run --- pyproject.toml | 1 + src/codegen/cli/commands/run/main.py | 10 +++ src/codegen/cli/commands/run/run_daemon.py | 87 +++++++++++++++++++ .../cli/commands/start/docker_container.py | 71 ++++++++++++--- .../cli/commands/start/docker_fleet.py | 20 ++--- src/codegen/cli/commands/start/main.py | 38 +++++--- .../git/repo_operator/repo_operator.py | 5 ++ src/codegen/runner/clients/client.py | 13 ++- src/codegen/runner/clients/codebase_client.py | 4 +- src/codegen/runner/clients/docker_client.py | 18 ++-- src/codegen/runner/models/apis.py | 24 ++--- .../runner/sandbox/ephemeral_server.py | 1 - src/codegen/runner/sandbox/middlewares.py | 34 ++++---- src/codegen/runner/sandbox/runner.py | 20 ----- src/codegen/runner/sandbox/server.py | 38 +------- src/codegen/runner/servers/local_daemon.py | 80 +++++++++++++++++ src/codegen/shared/logging/get_logger.py | 33 +++++++ .../codegen/runner/sandbox/test_runner.py | 4 - uv.lock | 16 +++- 19 files changed, 373 insertions(+), 144 deletions(-) create mode 100644 src/codegen/cli/commands/run/run_daemon.py create mode 100644 src/codegen/runner/servers/local_daemon.py create mode 100644 src/codegen/shared/logging/get_logger.py diff --git a/pyproject.toml b/pyproject.toml index 5f1fbe6b1..e28368c59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ dependencies = [ "httpx>=0.28.1", "docker>=6.1.3", "urllib3>=2.0.0", + "colorlog>=6.9.0", ] license = { text = "Apache-2.0" } diff --git a/src/codegen/cli/commands/run/main.py b/src/codegen/cli/commands/run/main.py index d34d93784..e42e18f7b 100644 --- a/src/codegen/cli/commands/run/main.py +++ b/src/codegen/cli/commands/run/main.py @@ -14,16 +14,22 @@ @requires_init @click.argument("label", required=True) @click.option("--web", is_flag=True, help="Run the function on the web service instead of locally") +@click.option("--daemon", is_flag=True, help="Run the function against a running daemon") @click.option("--diff-preview", type=int, help="Show a preview of the first N lines of the diff") @click.option("--arguments", type=str, help="Arguments as a json string to pass as the function's 'arguments' parameter") def run_command( session: CodegenSession, label: str, web: bool = False, + daemon: bool = False, diff_preview: int | None = None, arguments: str | None = None, ): """Run a codegen function by its label.""" + if web and daemon: + msg = "Cannot enable run on both the web and daemon" + raise ValueError(msg) + # Ensure venv is initialized venv = VenvManager(session.codegen_dir) if not venv.is_initialized(): @@ -54,6 +60,10 @@ def run_command( from codegen.cli.commands.run.run_cloud import run_cloud run_cloud(session, codemod, diff_preview=diff_preview) + elif daemon: + from codegen.cli.commands.run.run_daemon import run_daemon + + run_daemon(session, codemod, diff_preview=diff_preview) else: from codegen.cli.commands.run.run_local import run_local diff --git a/src/codegen/cli/commands/run/run_daemon.py b/src/codegen/cli/commands/run/run_daemon.py new file mode 100644 index 000000000..f54362c49 --- /dev/null +++ b/src/codegen/cli/commands/run/run_daemon.py @@ -0,0 +1,87 @@ +import rich +import rich_click as click +from rich.panel import Panel + +from codegen.cli.auth.session import CodegenSession +from codegen.cli.commands.start.docker_container import DockerContainer +from codegen.cli.errors import ServerError +from codegen.cli.rich.codeblocks import format_command +from codegen.cli.rich.spinners import create_spinner +from codegen.runner.clients.docker_client import DockerClient +from codegen.runner.enums.warmup_state import WarmupState + + +def run_daemon(session: CodegenSession, function, diff_preview: int | None = None): + """Run a function on the cloud service. + + Args: + session: The current codegen session + function: The function to run + diff_preview: Number of lines of diff to preview (None for all) + """ + with create_spinner(f"Running {function.name}...") as status: + try: + client = _get_docker_client(session) + run_output = client.run_function(function, commit=not diff_preview) + rich.print(f"✅ Ran {function.name} successfully") + + if run_output.logs: + rich.print("") + panel = Panel(run_output.logs, title="[bold]Logs[/bold]", border_style="blue", padding=(1, 2), expand=False) + rich.print(panel) + + if run_output.error: + rich.print("") + panel = Panel(run_output.error, title="[bold]Error[/bold]", border_style="red", padding=(1, 2), expand=False) + rich.print(panel) + + if run_output.observation: + # Only show diff preview if requested + if diff_preview: + rich.print("") # Add some spacing + + # Split and limit diff to requested number of lines + diff_lines = run_output.observation.splitlines() + truncated = len(diff_lines) > diff_preview + limited_diff = "\n".join(diff_lines[:diff_preview]) + + if truncated: + limited_diff += "\n\n...\n\n[yellow]diff truncated to {diff_preview} lines, view the full change set on your local file system after using run with `--apply-local`[/yellow]" + + panel = Panel(limited_diff, title="[bold]Diff Preview[/bold]", border_style="blue", padding=(1, 2), expand=False) + rich.print(panel) + else: + rich.print("") + rich.print("[yellow] No changes were produced by this codemod[/yellow]") + + if diff_preview: + rich.print("[green]✓ Changes have been applied to your local filesystem[/green]") + rich.print("[yellow]→ Don't forget to commit your changes:[/yellow]") + rich.print(format_command("git add .")) + rich.print(format_command("git commit -m 'Applied codemod changes'")) + + except ServerError as e: + status.stop() + raise click.ClickException(str(e)) + + +def _get_docker_client(session: CodegenSession) -> DockerClient: + repo_name = session.config.repository.name + if (container := DockerContainer.get(repo_name)) is None: + msg = f"Codegen runner does not exist for {repo_name}. Please run 'codegen start' from {session.config.repository.path}." + raise click.ClickException(msg) + + if not container.is_running(): + msg = f"Codegen runner for {repo_name} is not running. Please run 'codegen start' from {session.config.repository.path}." + raise click.ClickException(msg) + + client = DockerClient(container) + if not client.is_running(): + msg = "Codebase server is not running. Please stop the container and restart." + raise click.ClickException(msg) + + if client.server_info().warmup_state != WarmupState.COMPLETED: + msg = "Runner has not finished parsing the codebase. Please wait a moment and try again." + raise click.ClickException(msg) + + return client diff --git a/src/codegen/cli/commands/start/docker_container.py b/src/codegen/cli/commands/start/docker_container.py index 53ae3a339..4e5dade9e 100644 --- a/src/codegen/cli/commands/start/docker_container.py +++ b/src/codegen/cli/commands/start/docker_container.py @@ -1,31 +1,74 @@ +from functools import cached_property + import docker +from docker import DockerClient +from docker.errors import APIError, NotFound +from docker.models.containers import Container class DockerContainer: - _client: docker.DockerClient - host: str | None - port: int | None - name: str + _client: DockerClient + _container: Container | None - def __init__(self, client: docker.DockerClient, name: str, port: int | None = None, host: str | None = None): + def __init__(self, client: DockerClient, container: Container) -> None: self._client = client - self.host = host - self.port = port - self.name = name + self._container = container + + @classmethod + def get(cls, name: str) -> "DockerContainer | None": + try: + client = docker.from_env() + container = client.containers.get(name) + return cls(client=client, container=container) + except NotFound: + return None + + @cached_property + def name(self) -> str: + return self._container.name + + @cached_property + def host(self) -> str | None: + if not self.is_running(): + return None + + host_config = next(iter(self._container.ports.values()))[0] + return host_config["HostIp"] + + @cached_property + def port(self) -> int | None: + if not self.is_running(): + return None + + host_config = next(iter(self._container.ports.values()))[0] + return host_config["HostPort"] def is_running(self) -> bool: try: - container = self._client.containers.get(self.name) - return container.status == "running" - except docker.errors.NotFound: + return self._container.status == "running" + except NotFound: return False def start(self) -> bool: try: - container = self._client.containers.get(self.name) - container.start() + self._container.start() + return True + except (NotFound, APIError): + return False + + def stop(self) -> bool: + try: + self._container.stop() + return True + except (NotFound, APIError): + return False + + def remove(self) -> bool: + try: + self.stop() + self._container.remove() return True - except (docker.errors.NotFound, docker.errors.APIError): + except (NotFound, APIError): return False def __str__(self) -> str: diff --git a/src/codegen/cli/commands/start/docker_fleet.py b/src/codegen/cli/commands/start/docker_fleet.py index dd5907119..f2f885a4f 100644 --- a/src/codegen/cli/commands/start/docker_fleet.py +++ b/src/codegen/cli/commands/start/docker_fleet.py @@ -1,4 +1,5 @@ import docker +from docker.errors import NotFound from codegen.cli.commands.start.docker_container import DockerContainer @@ -15,19 +16,14 @@ def __init__(self, containers: list[DockerContainer]): def load(cls) -> "DockerFleet": try: client = docker.from_env() - containers = client.containers.list(all=True, filters={"ancestor": CODEGEN_RUNNER_IMAGE}) - codegen_containers = [] - for container in containers: + filters = {"ancestor": CODEGEN_RUNNER_IMAGE} + containers = [] + for container in client.containers.list(all=True, filters=filters): if container.attrs["Config"]["Image"] == CODEGEN_RUNNER_IMAGE: - if container.status == "running": - host_config = next(iter(container.ports.values()))[0] - codegen_container = DockerContainer(client=client, host=host_config["HostIp"], port=host_config["HostPort"], name=container.name) - else: - codegen_container = DockerContainer(client=client, name=container.name) - codegen_containers.append(codegen_container) - - return cls(containers=codegen_containers) - except docker.errors.NotFound: + containers.append(DockerContainer(client=client, container=container)) + + return cls(containers=containers) + except NotFound: return cls(containers=[]) @property diff --git a/src/codegen/cli/commands/start/main.py b/src/codegen/cli/commands/start/main.py index 4a4a531d0..9d9bc0049 100644 --- a/src/codegen/cli/commands/start/main.py +++ b/src/codegen/cli/commands/start/main.py @@ -8,7 +8,7 @@ from rich.panel import Panel from codegen.cli.commands.start.docker_container import DockerContainer -from codegen.cli.commands.start.docker_fleet import CODEGEN_RUNNER_IMAGE, DockerFleet +from codegen.cli.commands.start.docker_fleet import CODEGEN_RUNNER_IMAGE from codegen.configs.models.secrets import SecretsConfig from codegen.git.repo_operator.local_git_repo import LocalGitRepo from codegen.git.schemas.repo_config import RepoConfig @@ -20,13 +20,19 @@ @click.command(name="start") @click.option("--platform", "-t", type=click.Choice(["linux/amd64", "linux/arm64", "linux/amd64,linux/arm64"]), default="linux/amd64,linux/arm64", help="Target platform(s) for the Docker image") @click.option("--port", "-p", type=int, default=None, help="Port to run the server on") -def start_command(port: int | None, platform: str): +@click.option("--detached", "-d", is_flag=True, help="Run the server in detached mode") +@click.option("--skip-build", is_flag=True, help="Skip building the Docker image") +@click.option("--force", "-f", is_flag=True, help="Force start the server even if it is already running") +def start_command(port: int | None, platform: str, detached: bool = False, skip_build: bool = False, force: bool = False) -> None: """Starts a local codegen server""" repo_path = Path.cwd().resolve() repo_config = RepoConfig.from_repo_path(str(repo_path)) - fleet = DockerFleet.load() - if (container := fleet.get(repo_config.name)) is not None: - return _handle_existing_container(repo_config, container) + if (container := DockerContainer.get(repo_config.name)) is not None: + if force: + rich.print(f"[yellow]Removing existing runner {repo_config.name} to force restart[/yellow]") + container.remove() + else: + return _handle_existing_container(repo_config, container, force) codegen_version = version("codegen") rich.print(f"[bold green]Codegen version:[/bold green] {codegen_version}") @@ -35,10 +41,11 @@ def start_command(port: int | None, platform: str): port = get_free_port() try: - rich.print("[bold blue]Building Docker image...[/bold blue]") - _build_docker_image(codegen_root, platform) + if not skip_build: + rich.print("[bold blue]Building Docker image...[/bold blue]") + _build_docker_image(codegen_root, platform) rich.print("[bold blue]Starting Docker container...[/bold blue]") - _run_docker_container(repo_config, port) + _run_docker_container(repo_config, port, detached) rich.print(Panel(f"[green]Server started successfully![/green]\nAccess the server at: [bold]http://{_default_host}:{port}[/bold]", box=ROUNDED, title="Codegen Server")) # TODO: memory snapshot here except subprocess.CalledProcessError as e: @@ -49,7 +56,7 @@ def start_command(port: int | None, platform: str): raise click.Abort() -def _handle_existing_container(repo_config: RepoConfig, container: DockerContainer) -> None: +def _handle_existing_container(repo_config: RepoConfig, container: DockerContainer, force: bool) -> None: if container.is_running(): rich.print( Panel( @@ -86,7 +93,7 @@ def _build_docker_image(codegen_root: Path, platform: str) -> None: subprocess.run(build_cmd, check=True) -def _run_docker_container(repo_config: RepoConfig, port: int) -> None: +def _run_docker_container(repo_config: RepoConfig, port: int, detached: bool) -> None: container_repo_path = f"/app/git/{repo_config.name}" name_args = ["--name", f"{repo_config.name}"] envvars = { @@ -94,11 +101,18 @@ def _run_docker_container(repo_config: RepoConfig, port: int) -> None: "REPOSITORY_OWNER": LocalGitRepo(repo_config.repo_path).owner, "REPOSITORY_PATH": container_repo_path, "GITHUB_TOKEN": SecretsConfig().github_token, + "PYTHONUNBUFFERED": "1", # Ensure Python output is unbuffered } envvars_args = [arg for k, v in envvars.items() for arg in ("--env", f"{k}={v}")] mount_args = ["-v", f"{repo_config.repo_path}:{container_repo_path}"] - entry_point = f"uv run --frozen uvicorn codegen.runner.sandbox.server:app --host {_default_host} --port {port}" - run_cmd = ["docker", "run", "-d", "-p", f"{port}:{port}", *name_args, *mount_args, *envvars_args, CODEGEN_RUNNER_IMAGE, entry_point] + entry_point = f"uv run --frozen uvicorn codegen.runner.servers.local_daemon:app --host {_default_host} --port {port}" + port_args = ["-p", f"{port}:{port}"] + detached_args = ["-d"] if detached else [] + run_cmd = ["docker", "run", *detached_args, *port_args, *name_args, *mount_args, *envvars_args, CODEGEN_RUNNER_IMAGE, entry_point] rich.print(f"run_cmd: {str.join(' ', run_cmd)}") subprocess.run(run_cmd, check=True) + + if detached: + rich.print("[yellow]Container started in detached mode. To view logs, run:[/yellow]") + rich.print(f"[bold]docker logs -f {repo_config.name}[/bold]") diff --git a/src/codegen/git/repo_operator/repo_operator.py b/src/codegen/git/repo_operator/repo_operator.py index 0b66decf1..c978dd245 100644 --- a/src/codegen/git/repo_operator/repo_operator.py +++ b/src/codegen/git/repo_operator/repo_operator.py @@ -113,16 +113,20 @@ def viz_file_path(self) -> str: return os.path.join(self.viz_path, "graph.json") def _set_bot_email(self, git_cli: GitCLI) -> None: + logging.info(f"****** Setting bot email to {CODEGEN_BOT_EMAIL} ******") with git_cli.config_writer("repository") as writer: if not writer.has_section("user"): writer.add_section("user") writer.set("user", "email", CODEGEN_BOT_EMAIL) + logging.info(f"****** [DONE] Setting bot email to {CODEGEN_BOT_EMAIL} ******") def _set_bot_username(self, git_cli: GitCLI) -> None: + logging.info(f"****** Setting bot USERNAME to {CODEGEN_BOT_NAME} ******") with git_cli.config_writer("repository") as writer: if not writer.has_section("user"): writer.add_section("user") writer.set("user", "name", CODEGEN_BOT_NAME) + logging.info(f"****** [DONE] Setting bot USERNAME to {CODEGEN_BOT_NAME} ******") def _unset_bot_email(self, git_cli: GitCLI) -> None: with git_cli.config_writer("repository") as writer: @@ -136,6 +140,7 @@ def _unset_bot_username(self, git_cli: GitCLI) -> None: @cached_property def git_cli(self) -> GitCLI: + logger.info("**** Initializing git_cli!!!*****") git_cli = GitCLI(self.repo_path) username = None user_level = None diff --git a/src/codegen/runner/clients/client.py b/src/codegen/runner/clients/client.py index 555819f10..a45344db9 100644 --- a/src/codegen/runner/clients/client.py +++ b/src/codegen/runner/clients/client.py @@ -5,6 +5,8 @@ import requests from fastapi import params +from codegen.runner.models.apis import ServerInfo + logger = logging.getLogger(__name__) DEFAULT_SERVER_PORT = 4002 @@ -24,14 +26,21 @@ def __init__(self, host: str, port: int) -> None: self.port = port self.base_url = f"http://{host}:{port}" - def healthcheck(self, raise_on_error: bool = True) -> bool: + def is_running(self) -> bool: try: self.get("/") return True + except requests.exceptions.ConnectionError: + return False + + def server_info(self, raise_on_error: bool = False) -> ServerInfo: + try: + response = self.get("/") + return ServerInfo.model_validate(response.json()) except requests.exceptions.ConnectionError: if raise_on_error: raise - return False + return ServerInfo() def get(self, endpoint: str, data: dict | None = None) -> requests.Response: url = f"{self.base_url}{endpoint}" diff --git a/src/codegen/runner/clients/codebase_client.py b/src/codegen/runner/clients/codebase_client.py index b363b10ad..4aa15381e 100644 --- a/src/codegen/runner/clients/codebase_client.py +++ b/src/codegen/runner/clients/codebase_client.py @@ -57,7 +57,7 @@ def _wait_for_server(self, timeout: int = 30, interval: float = 0.3) -> None: """Wait for the server to start by polling the health endpoint""" start_time = time.time() while (time.time() - start_time) < timeout: - if self.healthcheck(raise_on_error=False): + if self.is_running(raise_on_error=False): return time.sleep(interval) msg = "Server failed to start within timeout period" @@ -80,4 +80,4 @@ def _get_envs(self) -> dict: test_config = RepoConfig.from_repo_path("/Users/caroljung/git/codegen/codegen-agi") test_config.full_name = "codegen-sh/codegen-agi" client = CodebaseClient(test_config) - print(client.healthcheck()) + print(client.is_running()) diff --git a/src/codegen/runner/clients/docker_client.py b/src/codegen/runner/clients/docker_client.py index eaf19fa36..e91877188 100644 --- a/src/codegen/runner/clients/docker_client.py +++ b/src/codegen/runner/clients/docker_client.py @@ -2,9 +2,10 @@ from codegen.cli.commands.start.docker_container import DockerContainer from codegen.cli.commands.start.docker_fleet import DockerFleet +from codegen.cli.utils.function_finder import DecoratedFunction from codegen.runner.clients.client import Client -from codegen.runner.models.apis import DIFF_ENDPOINT, GetDiffRequest -from codegen.runner.models.codemod import Codemod +from codegen.runner.models.apis import RUN_FUNCTION_ENDPOINT, RunFunctionRequest +from codegen.runner.models.codemod import CodemodRunResult class DockerClient(Client): @@ -16,6 +17,11 @@ def __init__(self, container: DockerContainer): raise Exception(msg) super().__init__(container.host, container.port) + def run_function(self, function: DecoratedFunction, commit: bool) -> CodemodRunResult: + req = RunFunctionRequest(function_name=function.name, codemod_source=function.source, commit=commit) + response = self.post(RUN_FUNCTION_ENDPOINT, req.model_dump()) + return CodemodRunResult.model_validate(response.json()) + if __name__ == "__main__": fleet = DockerFleet.load() @@ -24,8 +30,6 @@ def __init__(self, container: DockerContainer): msg = "No running container found. Run `codegen start` from a git repo first." raise Exception(msg) client = DockerClient(cur) - print(f"healthcheck: {client.healthcheck()}") - codemod = Codemod(user_code="print(codebase)") - diff_req = GetDiffRequest(codemod=codemod) - res = client.post(DIFF_ENDPOINT, diff_req.model_dump()) - print(res.json()) + print(f"healthcheck: {client.is_running()}") + result = client.run("print(codebase)") + print(result) diff --git a/src/codegen/runner/models/apis.py b/src/codegen/runner/models/apis.py index 17e125200..961a5c93b 100644 --- a/src/codegen/runner/models/apis.py +++ b/src/codegen/runner/models/apis.py @@ -9,9 +9,9 @@ EPHEMERAL_SANDBOX_SERVER_PORT = 4001 # APIs -SIGNAL_SHUTDOWN_ENDPOINT = "/signal_shutdown" DIFF_ENDPOINT = "/diff" BRANCH_ENDPOINT = "/branch" +RUN_FUNCTION_ENDPOINT = "/run" # Ephemeral sandbox apis RUN_ON_STRING_ENDPOINT = "/run_on_string" @@ -19,24 +19,10 @@ class ServerInfo(BaseModel): repo_name: str | None = None - is_running_codemod: bool = False - is_shutting_down: bool = False + synced_commit: str | None = None warmup_state: WarmupState = WarmupState.PENDING -class UtilizationMetrics(BaseModel): - timestamp: str - memory_rss_gb: float - memory_vms_gb: float - cpu_percent: float - threads_count: int - open_files_count: int - - -class SignalShutdownResponse(BaseModel): - is_ready_to_shutdown: bool - - class GetDiffRequest(BaseModel): codemod: Codemod max_transactions: int | None = None @@ -69,3 +55,9 @@ class GetRunOnStringRequest(BaseModel): class GetRunOnStringResult(BaseModel): result: CodemodRunResult + + +class RunFunctionRequest(BaseModel): + codemod_source: str + function_name: str + commit: bool = False diff --git a/src/codegen/runner/sandbox/ephemeral_server.py b/src/codegen/runner/sandbox/ephemeral_server.py index 1cfe8d248..fa60ac9aa 100644 --- a/src/codegen/runner/sandbox/ephemeral_server.py +++ b/src/codegen/runner/sandbox/ephemeral_server.py @@ -40,7 +40,6 @@ def health() -> ServerInfo: @app.post(RUN_ON_STRING_ENDPOINT) async def run_on_string(request: GetRunOnStringRequest) -> GetRunOnStringResult: - server_info.is_running_codemod = True logger.info(f"====[ run_on_string ]====\n> Codemod source: {request.codemod_source}\n> Input: {request.files}\n> Language: {request.language}\n") language = ProgrammingLanguage(request.language.upper()) with get_codebase_session(tmpdir=tempfile.mkdtemp(), files=request.files, programming_language=language) as codebase: diff --git a/src/codegen/runner/sandbox/middlewares.py b/src/codegen/runner/sandbox/middlewares.py index 52e797baf..34a236c01 100644 --- a/src/codegen/runner/sandbox/middlewares.py +++ b/src/codegen/runner/sandbox/middlewares.py @@ -1,18 +1,16 @@ import logging import traceback -from collections.abc import Callable -from functools import cached_property from http import HTTPStatus # Add this import -from typing import TypeVar +from typing import Callable, TypeVar from starlette.background import BackgroundTasks from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.requests import Request from starlette.responses import JSONResponse, Response -from codegen.runner.models.apis import ServerInfo from codegen.runner.sandbox.runner import SandboxRunner from codegen.shared.exceptions.compilation import UserCodeException +from codegen.shared.performance.stopwatch_utils import stopwatch logger = logging.getLogger(__name__) @@ -21,27 +19,21 @@ class CodemodRunMiddleware[TRequest, TResponse](BaseHTTPMiddleware): - def __init__(self, app, path: str, server_info_fn: Callable[[], ServerInfo], runner_fn: Callable[[], SandboxRunner]) -> None: + def __init__(self, app, path: str, runner_fn: Callable[[], SandboxRunner]) -> None: super().__init__(app) self.path = path - self.server_info_fn = server_info_fn self.runner_fn = runner_fn + @property + def runner(self) -> SandboxRunner: + return self.runner_fn() + async def dispatch(self, request: TRequest, call_next: RequestResponseEndpoint) -> TResponse: if request.url.path == self.path: return await self.process_request(request, call_next) return await call_next(request) - @cached_property - def server_info(self) -> ServerInfo: - return self.server_info_fn() - - @cached_property - def runner(self) -> SandboxRunner: - return self.runner_fn() - async def process_request(self, request: TRequest, call_next: RequestResponseEndpoint) -> TResponse: - self.server_info.is_running_codemod = True background_tasks = BackgroundTasks() try: logger.info(f"> (CodemodRunMiddleware) Request: {request.url.path}") @@ -54,7 +46,6 @@ async def process_request(self, request: TRequest, call_next: RequestResponseEnd except UserCodeException as e: message = f"Invalid user code for {request.url.path}" logger.info(message) - self.server_info.is_running_codemod = False return JSONResponse(status_code=HTTPStatus.BAD_REQUEST, content={"detail": message, "error": str(e), "traceback": traceback.format_exc()}) except Exception as e: @@ -70,5 +61,12 @@ async def cleanup_after_codemod(self, is_exception: bool = False): # TODO: instead of committing transactions, we should just rollback logger.info("Committing pending transactions due to exception") self.runner.codebase.ctx.commit_transactions(sync_graph=False) - self.runner.reset_runner() - self.server_info.is_running_codemod = False + await self.reset_runner() + + @stopwatch + async def reset_runner(self): + logger.info("=====[ reset_runner ]=====") + logger.info(f"Syncing runner to commit: {self.runner.commit} ...") + self.runner.codebase.checkout(commit=self.runner.commit) + self.runner.codebase.clean_repo() + self.runner.codebase.checkout(branch=self.runner.codebase.default_branch, create_if_missing=True) diff --git a/src/codegen/runner/sandbox/runner.py b/src/codegen/runner/sandbox/runner.py index ff9a242db..eace1af97 100644 --- a/src/codegen/runner/sandbox/runner.py +++ b/src/codegen/runner/sandbox/runner.py @@ -12,7 +12,6 @@ from codegen.sdk.codebase.factory.codebase_factory import CodebaseType from codegen.sdk.core.codebase import Codebase from codegen.shared.compilation.string_to_code import create_execute_function_from_codeblock -from codegen.shared.performance.stopwatch_utils import stopwatch logger = logging.getLogger(__name__) @@ -47,25 +46,6 @@ async def _build_graph(self) -> Codebase: projects = [ProjectConfig(programming_language=self.repo.language, repo_operator=self.op, base_path=self.repo.base_path, subdirectories=self.repo.subdirectories)] return Codebase(projects=projects) - @stopwatch - def reset_runner(self) -> None: - """Reset the runner to a cleaned/stable state for the next job. - - At the start of every job the runner should be in the following state: - - Codebase is checked out to the pinned commit (i.e. self.commit) - - Codebase RP (RepoOperator) has only the origin remote and no branches - - This method puts the runner in the above state and should be called at the end of every job. - """ - # TODO: move self.codebase.reset() here instead of during run - # TODO assert codebase is on the default branch and its clean - # TODO re-enable this (i.e. rather than pinning the runner commit, always move it forward to the latest commit) - logger.info("=====[ reset_runner ]=====") - logger.info(f"Syncing runner to commit: {self.commit} ...") - self.codebase.checkout(commit=self.commit) - self.codebase.clean_repo() - self.codebase.checkout(branch=self.codebase.default_branch, create_if_missing=True) - async def get_diff(self, request: GetDiffRequest) -> GetDiffResponse: custom_scope = {"context": request.codemod.codemod_context} if request.codemod.codemod_context else {} code_to_exec = create_execute_function_from_codeblock(codeblock=request.codemod.user_code, custom_scope=custom_scope) diff --git a/src/codegen/runner/sandbox/server.py b/src/codegen/runner/sandbox/server.py index 52b42cb99..f95a7790e 100644 --- a/src/codegen/runner/sandbox/server.py +++ b/src/codegen/runner/sandbox/server.py @@ -1,10 +1,7 @@ -import datetime as dt import logging import os from contextlib import asynccontextmanager -from datetime import datetime -import psutil from fastapi import FastAPI from codegen.configs.models.repository import RepositoryConfig @@ -13,19 +10,15 @@ from codegen.runner.models.apis import ( BRANCH_ENDPOINT, DIFF_ENDPOINT, - SIGNAL_SHUTDOWN_ENDPOINT, CreateBranchRequest, CreateBranchResponse, GetDiffRequest, GetDiffResponse, ServerInfo, - SignalShutdownResponse, - UtilizationMetrics, ) from codegen.runner.sandbox.middlewares import CodemodRunMiddleware from codegen.runner.sandbox.runner import SandboxRunner from codegen.shared.enums.programming_language import ProgrammingLanguage -from codegen.shared.performance.memory_utils import get_memory_stats logger = logging.getLogger(__name__) @@ -40,8 +33,9 @@ async def lifespan(server: FastAPI): try: default_repo_config = RepositoryConfig() - server_info = ServerInfo(repo_name=default_repo_config.full_name or default_repo_config.name) - logger.info(f"Starting up sandbox fastapi server for repo_name={server_info.repo_name}") + repo_name = default_repo_config.full_name or default_repo_config.name + server_info = ServerInfo(repo_name=repo_name) + logger.info(f"Starting up sandbox fastapi server for repo_name={repo_name}") repo_config = RepoConfig( name=default_repo_config.name, full_name=default_repo_config.full_name, @@ -51,6 +45,7 @@ async def lifespan(server: FastAPI): runner = SandboxRunner(repo_config=repo_config) server_info.warmup_state = WarmupState.PENDING await runner.warmup() + server_info.synced_commit = runner.commit.hexsha server_info.warmup_state = WarmupState.COMPLETED except Exception: logger.exception("Failed to build graph during warmup") @@ -65,13 +60,11 @@ async def lifespan(server: FastAPI): app.add_middleware( CodemodRunMiddleware[GetDiffRequest, GetDiffResponse], path=DIFF_ENDPOINT, - server_info_fn=lambda: server_info, runner_fn=lambda: runner, ) app.add_middleware( CodemodRunMiddleware[CreateBranchRequest, CreateBranchResponse], path=BRANCH_ENDPOINT, - server_info_fn=lambda: server_info, runner_fn=lambda: runner, ) @@ -81,29 +74,6 @@ def health() -> ServerInfo: return server_info -@app.get("/metrics/utilization", response_model=UtilizationMetrics) -async def utilization_metrics() -> UtilizationMetrics: - # Get the current process - process = psutil.Process(os.getpid()) - memory_stats = get_memory_stats() - - return UtilizationMetrics( - timestamp=datetime.now(dt.UTC).isoformat(), - memory_rss_gb=memory_stats.memory_rss_gb, - memory_vms_gb=memory_stats.memory_vms_gb, - cpu_percent=process.cpu_percent(), - threads_count=process.num_threads(), - open_files_count=len(process.open_files()), - ) - - -@app.post(SIGNAL_SHUTDOWN_ENDPOINT) -async def signal_shutdown() -> SignalShutdownResponse: - logger.info(f"repo_name={server_info.repo_name} received signal_shutdown") - server_info.is_shutting_down = True - return SignalShutdownResponse(is_ready_to_shutdown=not server_info.is_running_codemod) - - @app.post(DIFF_ENDPOINT) async def get_diff(request: GetDiffRequest) -> GetDiffResponse: return await runner.get_diff(request=request) diff --git a/src/codegen/runner/servers/local_daemon.py b/src/codegen/runner/servers/local_daemon.py new file mode 100644 index 000000000..62533f198 --- /dev/null +++ b/src/codegen/runner/servers/local_daemon.py @@ -0,0 +1,80 @@ +import logging +import os +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from codegen.configs.models.repository import RepositoryConfig +from codegen.git.schemas.repo_config import RepoConfig +from codegen.runner.enums.warmup_state import WarmupState +from codegen.runner.models.apis import ( + RUN_FUNCTION_ENDPOINT, + GetDiffRequest, + RunFunctionRequest, + ServerInfo, +) +from codegen.runner.models.codemod import Codemod, CodemodRunResult +from codegen.runner.sandbox.runner import SandboxRunner +from codegen.shared.enums.programming_language import ProgrammingLanguage + +# Configure logging at module level +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + force=True, +) +logger = logging.getLogger(__name__) + +server_info: ServerInfo +runner: SandboxRunner + + +@asynccontextmanager +async def lifespan(server: FastAPI): + global server_info + global runner + + try: + default_repo_config = RepositoryConfig() + repo_name = default_repo_config.full_name or default_repo_config.name + server_info = ServerInfo(repo_name=repo_name) + logger.info(f"Starting up sandbox fastapi server for repo_name={repo_name}") + repo_config = RepoConfig( + name=default_repo_config.name, + full_name=default_repo_config.full_name, + base_dir=os.path.dirname(default_repo_config.path), + language=ProgrammingLanguage(default_repo_config.language.upper()), + ) + runner = SandboxRunner(repo_config=repo_config) + server_info.warmup_state = WarmupState.PENDING + await runner.warmup() + server_info.synced_commit = runner.commit.hexsha + server_info.warmup_state = WarmupState.COMPLETED + except Exception: + logger.exception("Failed to build graph during warmup") + server_info.warmup_state = WarmupState.FAILED + + logger.info("Local daemon is ready to accept requests!") + yield + logger.info("Shutting down local daemon server") + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def health() -> ServerInfo: + return server_info + + +@app.post(RUN_FUNCTION_ENDPOINT) +async def run(request: RunFunctionRequest) -> CodemodRunResult: + # TODO: Sync graph to whatever changes are in the repo currently + + # Run the request + diff_req = GetDiffRequest(codemod=Codemod(user_code=request.codemod_source)) + diff_response = await runner.get_diff(request=diff_req) + if request.commit: + commit_sha = runner.codebase.git_commit(f"[Codegen] {request.function_name}") + logger.info(f"Committed changes to {commit_sha.hexsha}") + return diff_response.result diff --git a/src/codegen/shared/logging/get_logger.py b/src/codegen/shared/logging/get_logger.py new file mode 100644 index 000000000..0e287412a --- /dev/null +++ b/src/codegen/shared/logging/get_logger.py @@ -0,0 +1,33 @@ +import logging + +import colorlog + +handler = colorlog.StreamHandler() +handler.setFormatter( + colorlog.ColoredFormatter( + "%(white)s%(asctime)s - %(name)s - %(log_color)s%(levelname)s%(reset)s%(white)s - %(message_log_color)s%(message)s", + log_colors={ + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red,bg_white", + }, + secondary_log_colors={ + "message": { + "DEBUG": "cyan", + "INFO": "blue", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red,bg_white", + } + }, + ) +) + + +def get_logger(name: str, level: int = logging.INFO) -> logging.Logger: + logger = logging.getLogger(name) + logger.addHandler(handler) + logger.setLevel(level) + return logger diff --git a/tests/unit/codegen/runner/sandbox/test_runner.py b/tests/unit/codegen/runner/sandbox/test_runner.py index 1935e9b3e..45d0058c3 100644 --- a/tests/unit/codegen/runner/sandbox/test_runner.py +++ b/tests/unit/codegen/runner/sandbox/test_runner.py @@ -54,10 +54,6 @@ async def test_sandbox_runner_reset_runner_deletes_branches(mock_branch, mock_ex runner.codebase.checkout(branch="test-branch-a", create_if_missing=True) runner.codebase.checkout(branch="test-branch-b", create_if_missing=True) assert len(runner.codebase._op.git_cli.heads) == num_branches + 2 - runner.reset_runner() - assert len(runner.codebase._op.git_cli.heads) == 1 # now should be on default branch at self.commit - assert runner.codebase._op.git_cli.active_branch.name == runner.codebase.default_branch - assert runner.codebase._op.git_cli.head.commit == runner.commit # @pytest.mark.asyncio diff --git a/uv.lock b/uv.lock index 39b85167b..b61e28200 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.12, <3.14" resolution-markers = [ "python_full_version >= '3.12.4'", @@ -543,6 +542,7 @@ dependencies = [ { name = "astor" }, { name = "click" }, { name = "codeowners" }, + { name = "colorlog" }, { name = "dataclasses-json" }, { name = "datamodel-code-generator" }, { name = "dicttoxml" }, @@ -667,6 +667,7 @@ requires-dist = [ { name = "attrs", marker = "extra == 'lsp'", specifier = ">=25.1.0" }, { name = "click", specifier = ">=8.1.7" }, { name = "codeowners", specifier = ">=0.6.0,<1.0.0" }, + { name = "colorlog", specifier = ">=6.9.0" }, { name = "dataclasses-json", specifier = ">=0.6.4,<1.0.0" }, { name = "datamodel-code-generator", specifier = ">=0.26.5" }, { name = "dicttoxml", specifier = ">=1.7.16,<2.0.0" }, @@ -739,7 +740,6 @@ requires-dist = [ { name = "wrapt", specifier = ">=1.16.0,<2.0.0" }, { name = "xmltodict", specifier = ">=0.13.0,<1.0.0" }, ] -provides-extras = ["lsp", "types"] [package.metadata.requires-dev] dev = [ @@ -800,6 +800,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "colorlog" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424 }, +] + [[package]] name = "comm" version = "0.2.2" From 1462286db508a27107b0d60f9ad55955fe3b9cb8 Mon Sep 17 00:00:00 2001 From: Carol Jung Date: Wed, 26 Feb 2025 14:32:03 -0800 Subject: [PATCH 2/4] wip --- src/codegen/git/repo_operator/repo_operator.py | 7 +++---- src/codegen/runner/servers/local_daemon.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/codegen/git/repo_operator/repo_operator.py b/src/codegen/git/repo_operator/repo_operator.py index c978dd245..eee044091 100644 --- a/src/codegen/git/repo_operator/repo_operator.py +++ b/src/codegen/git/repo_operator/repo_operator.py @@ -113,20 +113,16 @@ def viz_file_path(self) -> str: return os.path.join(self.viz_path, "graph.json") def _set_bot_email(self, git_cli: GitCLI) -> None: - logging.info(f"****** Setting bot email to {CODEGEN_BOT_EMAIL} ******") with git_cli.config_writer("repository") as writer: if not writer.has_section("user"): writer.add_section("user") writer.set("user", "email", CODEGEN_BOT_EMAIL) - logging.info(f"****** [DONE] Setting bot email to {CODEGEN_BOT_EMAIL} ******") def _set_bot_username(self, git_cli: GitCLI) -> None: - logging.info(f"****** Setting bot USERNAME to {CODEGEN_BOT_NAME} ******") with git_cli.config_writer("repository") as writer: if not writer.has_section("user"): writer.add_section("user") writer.set("user", "name", CODEGEN_BOT_NAME) - logging.info(f"****** [DONE] Setting bot USERNAME to {CODEGEN_BOT_NAME} ******") def _unset_bot_email(self, git_cli: GitCLI) -> None: with git_cli.config_writer("repository") as writer: @@ -476,8 +472,11 @@ def commit_changes(self, message: str, verify: bool = False) -> bool: staged_changes = self.git_cli.git.diff("--staged") if staged_changes: commit_args = ["-m", message] + if self.bot_commit: + commit_args.append(f"--author='{CODEGEN_BOT_NAME} <{CODEGEN_BOT_EMAIL}>'") if not verify: commit_args.append("--no-verify") + logger.info(f"****** Committing changes with args: {commit_args} ******") self.git_cli.git.commit(*commit_args) return True else: diff --git a/src/codegen/runner/servers/local_daemon.py b/src/codegen/runner/servers/local_daemon.py index 62533f198..a3520be80 100644 --- a/src/codegen/runner/servers/local_daemon.py +++ b/src/codegen/runner/servers/local_daemon.py @@ -19,7 +19,7 @@ # Configure logging at module level logging.basicConfig( - level=logging.DEBUG, + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", force=True, ) From 45a968229edd3aa37a715b53c9a0b652569ad2c6 Mon Sep 17 00:00:00 2001 From: Carol Jung Date: Wed, 26 Feb 2025 14:42:37 -0800 Subject: [PATCH 3/4] set git user config from dockerfile --- Dockerfile-runner | 4 ++++ src/codegen/git/repo_operator/repo_operator.py | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile-runner b/Dockerfile-runner index 1ec5ca086..4934d83cc 100644 --- a/Dockerfile-runner +++ b/Dockerfile-runner @@ -21,6 +21,10 @@ RUN apt-get update && apt-get install -y \ # Cleanup apt cache to reduce image size && rm -rf /var/lib/apt/lists/* +# Set git config +RUN git config --global user.email "team+codegenbot@codegen.sh" \ + && git config --global user.name "codegen-bot" + # Install nvm and Node.js SHELL ["/bin/bash", "-c"] RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \ diff --git a/src/codegen/git/repo_operator/repo_operator.py b/src/codegen/git/repo_operator/repo_operator.py index eee044091..81756ca13 100644 --- a/src/codegen/git/repo_operator/repo_operator.py +++ b/src/codegen/git/repo_operator/repo_operator.py @@ -136,7 +136,6 @@ def _unset_bot_username(self, git_cli: GitCLI) -> None: @cached_property def git_cli(self) -> GitCLI: - logger.info("**** Initializing git_cli!!!*****") git_cli = GitCLI(self.repo_path) username = None user_level = None @@ -476,7 +475,6 @@ def commit_changes(self, message: str, verify: bool = False) -> bool: commit_args.append(f"--author='{CODEGEN_BOT_NAME} <{CODEGEN_BOT_EMAIL}>'") if not verify: commit_args.append("--no-verify") - logger.info(f"****** Committing changes with args: {commit_args} ******") self.git_cli.git.commit(*commit_args) return True else: From 0c2f68013750d9d6cbec5a5aab1561d613c06f28 Mon Sep 17 00:00:00 2001 From: Carol Jung Date: Wed, 26 Feb 2025 15:03:25 -0800 Subject: [PATCH 4/4] fixesss --- src/codegen/git/schemas/repo_config.py | 11 ++++++++ src/codegen/runner/clients/codebase_client.py | 2 +- src/codegen/runner/clients/docker_client.py | 5 ++++ src/codegen/runner/servers/local_daemon.py | 25 +++++++++---------- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/codegen/git/schemas/repo_config.py b/src/codegen/git/schemas/repo_config.py index 1386144cf..966bef6b7 100644 --- a/src/codegen/git/schemas/repo_config.py +++ b/src/codegen/git/schemas/repo_config.py @@ -4,6 +4,7 @@ from pydantic import BaseModel +from codegen.configs.models.repository import RepositoryConfig from codegen.git.schemas.enums import RepoVisibility from codegen.shared.enums.programming_language import ProgrammingLanguage @@ -24,6 +25,16 @@ class RepoConfig(BaseModel): base_path: str | None = None # root directory of the codebase within the repo subdirectories: list[str] | None = None + @classmethod + def from_envs(cls) -> "RepoConfig": + default_repo_config = RepositoryConfig() + return RepoConfig( + name=default_repo_config.name, + full_name=default_repo_config.full_name, + base_dir=os.path.dirname(default_repo_config.path), + language=ProgrammingLanguage(default_repo_config.language.upper()), + ) + @classmethod def from_repo_path(cls, repo_path: str) -> "RepoConfig": name = os.path.basename(repo_path) diff --git a/src/codegen/runner/clients/codebase_client.py b/src/codegen/runner/clients/codebase_client.py index 4aa15381e..6f8da4791 100644 --- a/src/codegen/runner/clients/codebase_client.py +++ b/src/codegen/runner/clients/codebase_client.py @@ -57,7 +57,7 @@ def _wait_for_server(self, timeout: int = 30, interval: float = 0.3) -> None: """Wait for the server to start by polling the health endpoint""" start_time = time.time() while (time.time() - start_time) < timeout: - if self.is_running(raise_on_error=False): + if self.is_running(): return time.sleep(interval) msg = "Server failed to start within timeout period" diff --git a/src/codegen/runner/clients/docker_client.py b/src/codegen/runner/clients/docker_client.py index e91877188..89b8a1844 100644 --- a/src/codegen/runner/clients/docker_client.py +++ b/src/codegen/runner/clients/docker_client.py @@ -17,6 +17,11 @@ def __init__(self, container: DockerContainer): raise Exception(msg) super().__init__(container.host, container.port) + def run(self, codemod_source: str, commit: bool | None = None) -> CodemodRunResult: + req = RunFunctionRequest(function_name="unnamed", codemod_source=codemod_source, commit=commit) + response = self.post(RUN_FUNCTION_ENDPOINT, req.model_dump()) + return CodemodRunResult.model_validate(response.json()) + def run_function(self, function: DecoratedFunction, commit: bool) -> CodemodRunResult: req = RunFunctionRequest(function_name=function.name, codemod_source=function.source, commit=commit) response = self.post(RUN_FUNCTION_ENDPOINT, req.model_dump()) diff --git a/src/codegen/runner/servers/local_daemon.py b/src/codegen/runner/servers/local_daemon.py index a3520be80..1c2e5206c 100644 --- a/src/codegen/runner/servers/local_daemon.py +++ b/src/codegen/runner/servers/local_daemon.py @@ -1,10 +1,9 @@ import logging -import os from contextlib import asynccontextmanager from fastapi import FastAPI -from codegen.configs.models.repository import RepositoryConfig +from codegen.git.configs.constants import CODEGEN_BOT_EMAIL, CODEGEN_BOT_NAME from codegen.git.schemas.repo_config import RepoConfig from codegen.runner.enums.warmup_state import WarmupState from codegen.runner.models.apis import ( @@ -15,7 +14,6 @@ ) from codegen.runner.models.codemod import Codemod, CodemodRunResult from codegen.runner.sandbox.runner import SandboxRunner -from codegen.shared.enums.programming_language import ProgrammingLanguage # Configure logging at module level logging.basicConfig( @@ -35,21 +33,22 @@ async def lifespan(server: FastAPI): global runner try: - default_repo_config = RepositoryConfig() - repo_name = default_repo_config.full_name or default_repo_config.name - server_info = ServerInfo(repo_name=repo_name) - logger.info(f"Starting up sandbox fastapi server for repo_name={repo_name}") - repo_config = RepoConfig( - name=default_repo_config.name, - full_name=default_repo_config.full_name, - base_dir=os.path.dirname(default_repo_config.path), - language=ProgrammingLanguage(default_repo_config.language.upper()), - ) + repo_config = RepoConfig.from_envs() + server_info = ServerInfo(repo_name=repo_config.full_name or repo_config.name) + + # Set the bot email and username + logger.info(f"Configuring git user config to {CODEGEN_BOT_EMAIL} and {CODEGEN_BOT_NAME}") runner = SandboxRunner(repo_config=repo_config) + runner.op.git_cli.git.config("user.email", CODEGEN_BOT_EMAIL) + runner.op.git_cli.git.config("user.name", CODEGEN_BOT_NAME) + + # Parse the codebase + logger.info(f"Starting up sandbox fastapi server for repo_name={repo_config.name}") server_info.warmup_state = WarmupState.PENDING await runner.warmup() server_info.synced_commit = runner.commit.hexsha server_info.warmup_state = WarmupState.COMPLETED + except Exception: logger.exception("Failed to build graph during warmup") server_info.warmup_state = WarmupState.FAILED