Merge branch 'main' into copilot/fix-6542

This commit is contained in:
Eric Zhu 2025-05-21 20:30:48 -07:00 committed by GitHub
commit b3e4b44a70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 98 additions and 20 deletions

View File

@ -34,7 +34,6 @@ html[data-theme="dark"] {
} }
/* Adding header icon hover and focus effects */ /* Adding header icon hover and focus effects */
.bd-header a:hover,
.bd-header a:focus-visible { .bd-header a:focus-visible {
color: var(--pst-color-secondary) !important; color: var(--pst-color-secondary) !important;
text-decoration: underline !important; text-decoration: underline !important;

View File

@ -31,6 +31,7 @@ class ModelFamily:
GEMINI_1_5_PRO = "gemini-1.5-pro" GEMINI_1_5_PRO = "gemini-1.5-pro"
GEMINI_2_0_FLASH = "gemini-2.0-flash" GEMINI_2_0_FLASH = "gemini-2.0-flash"
GEMINI_2_5_PRO = "gemini-2.5-pro" GEMINI_2_5_PRO = "gemini-2.5-pro"
GEMINI_2_5_FLASH = "gemini-2.5-flash"
CLAUDE_3_HAIKU = "claude-3-haiku" CLAUDE_3_HAIKU = "claude-3-haiku"
CLAUDE_3_SONNET = "claude-3-sonnet" CLAUDE_3_SONNET = "claude-3-sonnet"
CLAUDE_3_OPUS = "claude-3-opus" CLAUDE_3_OPUS = "claude-3-opus"
@ -64,6 +65,7 @@ class ModelFamily:
"gemini-1.5-pro", "gemini-1.5-pro",
"gemini-2.0-flash", "gemini-2.0-flash",
"gemini-2.5-pro", "gemini-2.5-pro",
"gemini-2.5-flash"
# anthropic_models # anthropic_models
"claude-3-haiku", "claude-3-haiku",
"claude-3-sonnet", "claude-3-sonnet",
@ -107,6 +109,7 @@ class ModelFamily:
ModelFamily.GEMINI_1_5_PRO, ModelFamily.GEMINI_1_5_PRO,
ModelFamily.GEMINI_2_0_FLASH, ModelFamily.GEMINI_2_0_FLASH,
ModelFamily.GEMINI_2_5_PRO, ModelFamily.GEMINI_2_5_PRO,
ModelFamily.GEMINI_2_5_FLASH,
) )
@staticmethod @staticmethod

View File

@ -236,7 +236,10 @@ class DockerJupyterCodeExecutor(CodeExecutor, Component[DockerJupyterCodeExecuto
else: else:
outputs.append(json.dumps(data.data)) outputs.append(json.dumps(data.data))
else: else:
return DockerJupyterCodeResult(exit_code=1, output=f"ERROR: {result.output}", output_files=output_files) existing_output = "\n".join([str(output) for output in outputs])
return DockerJupyterCodeResult(
exit_code=1, output=existing_output + "\nERROR: " + result.output, output_files=output_files
)
return DockerJupyterCodeResult( return DockerJupyterCodeResult(
exit_code=0, output="\n".join([str(output) for output in outputs]), output_files=output_files exit_code=0, output="\n".join([str(output) for output in outputs]), output_files=output_files
) )

View File

@ -39,6 +39,7 @@ class LocalCommandLineCodeExecutorConfig(BaseModel):
timeout: int = 60 timeout: int = 60
work_dir: Optional[str] = None work_dir: Optional[str] = None
functions_module: str = "functions" functions_module: str = "functions"
cleanup_temp_files: bool = True
class LocalCommandLineCodeExecutor(CodeExecutor, Component[LocalCommandLineCodeExecutorConfig]): class LocalCommandLineCodeExecutor(CodeExecutor, Component[LocalCommandLineCodeExecutorConfig]):
@ -78,6 +79,7 @@ class LocalCommandLineCodeExecutor(CodeExecutor, Component[LocalCommandLineCodeE
a default working directory will be used. The default working directory is a temporary directory. a default working directory will be used. The default working directory is a temporary directory.
functions (List[Union[FunctionWithRequirements[Any, A], Callable[..., Any]]]): A list of functions that are available to the code executor. Default is an empty list. functions (List[Union[FunctionWithRequirements[Any, A], Callable[..., Any]]]): A list of functions that are available to the code executor. Default is an empty list.
functions_module (str, optional): The name of the module that will be created to store the functions. Defaults to "functions". functions_module (str, optional): The name of the module that will be created to store the functions. Defaults to "functions".
cleanup_temp_files (bool, optional): Whether to automatically clean up temporary files after execution. Defaults to True.
virtual_env_context (Optional[SimpleNamespace], optional): The virtual environment context. Defaults to None. virtual_env_context (Optional[SimpleNamespace], optional): The virtual environment context. Defaults to None.
.. note:: .. note::
@ -154,10 +156,12 @@ $functions"""
] ]
] = [], ] = [],
functions_module: str = "functions", functions_module: str = "functions",
cleanup_temp_files: bool = True,
virtual_env_context: Optional[SimpleNamespace] = None, virtual_env_context: Optional[SimpleNamespace] = None,
): ):
if timeout < 1: if timeout < 1:
raise ValueError("Timeout must be greater than or equal to 1.") raise ValueError("Timeout must be greater than or equal to 1.")
self._timeout = timeout
self._work_dir: Optional[Path] = None self._work_dir: Optional[Path] = None
if work_dir is not None: if work_dir is not None:
@ -174,13 +178,6 @@ $functions"""
self._work_dir = work_dir self._work_dir = work_dir
self._work_dir.mkdir(exist_ok=True) self._work_dir.mkdir(exist_ok=True)
if not functions_module.isidentifier():
raise ValueError("Module name must be a valid Python identifier")
self._functions_module = functions_module
self._timeout = timeout
self._functions = functions self._functions = functions
# Setup could take some time so we intentionally wait for the first code block to do it. # Setup could take some time so we intentionally wait for the first code block to do it.
if len(functions) > 0: if len(functions) > 0:
@ -188,6 +185,11 @@ $functions"""
else: else:
self._setup_functions_complete = True self._setup_functions_complete = True
if not functions_module.isidentifier():
raise ValueError("Module name must be a valid Python identifier")
self._functions_module = functions_module
self._cleanup_temp_files = cleanup_temp_files
self._virtual_env_context: Optional[SimpleNamespace] = virtual_env_context self._virtual_env_context: Optional[SimpleNamespace] = virtual_env_context
self._temp_dir: Optional[tempfile.TemporaryDirectory[str]] = None self._temp_dir: Optional[tempfile.TemporaryDirectory[str]] = None
@ -228,15 +230,6 @@ $functions"""
functions="\n\n".join([to_stub(func) for func in self._functions]), functions="\n\n".join([to_stub(func) for func in self._functions]),
) )
@property
def functions_module(self) -> str:
"""(Experimental) The module name for the functions."""
return self._functions_module
@property
def functions(self) -> List[str]:
raise NotImplementedError
@property @property
def timeout(self) -> int: def timeout(self) -> int:
"""(Experimental) The timeout for code execution.""" """(Experimental) The timeout for code execution."""
@ -254,6 +247,20 @@ $functions"""
self._started = True self._started = True
return Path(self._temp_dir.name) return Path(self._temp_dir.name)
@property
def functions(self) -> List[str]:
raise NotImplementedError
@property
def functions_module(self) -> str:
"""(Experimental) The module name for the functions."""
return self._functions_module
@property
def cleanup_temp_files(self) -> bool:
"""(Experimental) Whether to automatically clean up temporary files after execution."""
return self._cleanup_temp_files
async def _setup_functions(self, cancellation_token: CancellationToken) -> None: async def _setup_functions(self, cancellation_token: CancellationToken) -> None:
func_file_content = build_python_functions_file(self._functions) func_file_content = build_python_functions_file(self._functions)
func_file = self.work_dir / f"{self._functions_module}.py" func_file = self.work_dir / f"{self._functions_module}.py"
@ -446,7 +453,16 @@ $functions"""
break break
code_file = str(file_names[0]) if file_names else None code_file = str(file_names[0]) if file_names else None
return CommandLineCodeResult(exit_code=exitcode, output=logs_all, code_file=code_file) code_result = CommandLineCodeResult(exit_code=exitcode, output=logs_all, code_file=code_file)
if self._cleanup_temp_files:
for file in file_names:
try:
file.unlink(missing_ok=True)
except OSError as error:
logging.error(f"Failed to delete temporary file {file}: {error}")
return code_result
async def restart(self) -> None: async def restart(self) -> None:
"""(Experimental) Restart the code executor.""" """(Experimental) Restart the code executor."""
@ -488,6 +504,7 @@ $functions"""
timeout=self._timeout, timeout=self._timeout,
work_dir=str(self.work_dir), work_dir=str(self.work_dir),
functions_module=self._functions_module, functions_module=self._functions_module,
cleanup_temp_files=self._cleanup_temp_files,
) )
@classmethod @classmethod
@ -496,4 +513,5 @@ $functions"""
timeout=config.timeout, timeout=config.timeout,
work_dir=Path(config.work_dir) if config.work_dir is not None else None, work_dir=Path(config.work_dir) if config.work_dir is not None else None,
functions_module=config.functions_module, functions_module=config.functions_module,
cleanup_temp_files=config.cleanup_temp_files,
) )

View File

@ -308,6 +308,14 @@ _MODEL_INFO: Dict[str, ModelInfo] = {
"structured_output": True, "structured_output": True,
"multiple_system_messages": False, "multiple_system_messages": False,
}, },
"gemini-2.5-flash-preview-05-20": {
"vision": True,
"function_calling": True,
"json_output": True,
"family": ModelFamily.GEMINI_2_5_FLASH,
"structured_output": True,
"multiple_system_messages": False,
},
"claude-3-haiku-20240307": { "claude-3-haiku-20240307": {
"vision": True, "vision": True,
"function_calling": True, "function_calling": True,
@ -422,6 +430,7 @@ _MODEL_TOKEN_LIMITS: Dict[str, int] = {
"gemini-2.0-flash": 1048576, "gemini-2.0-flash": 1048576,
"gemini-2.0-flash-lite-preview-02-05": 1048576, "gemini-2.0-flash-lite-preview-02-05": 1048576,
"gemini-2.5-pro-preview-03-25": 2097152, "gemini-2.5-pro-preview-03-25": 2097152,
"gemini-2.5-flash-preview-05-20": 1048576,
"claude-3-haiku-20240307": 50000, "claude-3-haiku-20240307": 50000,
"claude-3-sonnet-20240229": 40000, "claude-3-sonnet-20240229": 40000,
"claude-3-opus-20240229": 20000, "claude-3-opus-20240229": 20000,

View File

@ -12,6 +12,7 @@ import types
import venv import venv
from pathlib import Path from pathlib import Path
from typing import AsyncGenerator, TypeAlias from typing import AsyncGenerator, TypeAlias
from unittest.mock import patch
import pytest import pytest
import pytest_asyncio import pytest_asyncio
@ -109,7 +110,7 @@ async def executor_and_temp_dir(
request: pytest.FixtureRequest, request: pytest.FixtureRequest,
) -> AsyncGenerator[tuple[LocalCommandLineCodeExecutor, str], None]: ) -> AsyncGenerator[tuple[LocalCommandLineCodeExecutor, str], None]:
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
executor = LocalCommandLineCodeExecutor(work_dir=temp_dir) executor = LocalCommandLineCodeExecutor(work_dir=temp_dir, cleanup_temp_files=False)
await executor.start() await executor.start()
yield executor, temp_dir yield executor, temp_dir
@ -399,3 +400,48 @@ async def test_ps1_script(executor_and_temp_dir: ExecutorFixture) -> None:
assert result.exit_code == 0 assert result.exit_code == 0
assert "hello from powershell!" in result.output assert "hello from powershell!" in result.output
assert result.code_file is not None assert result.code_file is not None
@pytest.mark.asyncio
async def test_cleanup_temp_files_behavior() -> None:
with tempfile.TemporaryDirectory() as temp_dir:
# Test with cleanup_temp_files=True (default)
executor = LocalCommandLineCodeExecutor(work_dir=temp_dir, cleanup_temp_files=True)
await executor.start()
cancellation_token = CancellationToken()
code_blocks = [CodeBlock(code="print('cleanup test')", language="python")]
result = await executor.execute_code_blocks(code_blocks, cancellation_token)
assert result.exit_code == 0
assert "cleanup test" in result.output
# The code file should have been deleted
assert result.code_file is not None
assert not Path(result.code_file).exists()
# Test with cleanup_temp_files=False
executor = LocalCommandLineCodeExecutor(work_dir=temp_dir, cleanup_temp_files=False)
await executor.start()
cancellation_token = CancellationToken()
code_blocks = [CodeBlock(code="print('no cleanup')", language="python")]
result = await executor.execute_code_blocks(code_blocks, cancellation_token)
assert result.exit_code == 0
assert "no cleanup" in result.output
# The code file should still exist
assert result.code_file is not None
assert Path(result.code_file).exists()
@pytest.mark.asyncio
async def test_cleanup_temp_files_oserror(caplog: pytest.LogCaptureFixture) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
executor = LocalCommandLineCodeExecutor(work_dir=temp_dir, cleanup_temp_files=True)
await executor.start()
cancellation_token = CancellationToken()
code_blocks = [CodeBlock(code="print('cleanup test')", language="python")]
# Patch Path.unlink to raise OSError for this test
with patch("pathlib.Path.unlink", side_effect=OSError("Mocked OSError")):
with caplog.at_level("ERROR"):
await executor.execute_code_blocks(code_blocks, cancellation_token)
# The code file should have been attempted to be deleted and failed
assert any("Failed to delete temporary file" in record.message for record in caplog.records)
assert any("Mocked OSError" in record.message for record in caplog.records)