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 */
.bd-header a:hover,
.bd-header a:focus-visible {
color: var(--pst-color-secondary) !important;
text-decoration: underline !important;

View File

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

View File

@ -236,7 +236,10 @@ class DockerJupyterCodeExecutor(CodeExecutor, Component[DockerJupyterCodeExecuto
else:
outputs.append(json.dumps(data.data))
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(
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
work_dir: Optional[str] = None
functions_module: str = "functions"
cleanup_temp_files: bool = True
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.
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".
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.
.. note::
@ -154,10 +156,12 @@ $functions"""
]
] = [],
functions_module: str = "functions",
cleanup_temp_files: bool = True,
virtual_env_context: Optional[SimpleNamespace] = None,
):
if timeout < 1:
raise ValueError("Timeout must be greater than or equal to 1.")
self._timeout = timeout
self._work_dir: Optional[Path] = None
if work_dir is not None:
@ -174,13 +178,6 @@ $functions"""
self._work_dir = work_dir
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
# Setup could take some time so we intentionally wait for the first code block to do it.
if len(functions) > 0:
@ -188,6 +185,11 @@ $functions"""
else:
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._temp_dir: Optional[tempfile.TemporaryDirectory[str]] = None
@ -228,15 +230,6 @@ $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
def timeout(self) -> int:
"""(Experimental) The timeout for code execution."""
@ -254,6 +247,20 @@ $functions"""
self._started = True
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:
func_file_content = build_python_functions_file(self._functions)
func_file = self.work_dir / f"{self._functions_module}.py"
@ -446,7 +453,16 @@ $functions"""
break
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:
"""(Experimental) Restart the code executor."""
@ -488,6 +504,7 @@ $functions"""
timeout=self._timeout,
work_dir=str(self.work_dir),
functions_module=self._functions_module,
cleanup_temp_files=self._cleanup_temp_files,
)
@classmethod
@ -496,4 +513,5 @@ $functions"""
timeout=config.timeout,
work_dir=Path(config.work_dir) if config.work_dir is not None else None,
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,
"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": {
"vision": True,
"function_calling": True,
@ -422,6 +430,7 @@ _MODEL_TOKEN_LIMITS: Dict[str, int] = {
"gemini-2.0-flash": 1048576,
"gemini-2.0-flash-lite-preview-02-05": 1048576,
"gemini-2.5-pro-preview-03-25": 2097152,
"gemini-2.5-flash-preview-05-20": 1048576,
"claude-3-haiku-20240307": 50000,
"claude-3-sonnet-20240229": 40000,
"claude-3-opus-20240229": 20000,

View File

@ -12,6 +12,7 @@ import types
import venv
from pathlib import Path
from typing import AsyncGenerator, TypeAlias
from unittest.mock import patch
import pytest
import pytest_asyncio
@ -109,7 +110,7 @@ async def executor_and_temp_dir(
request: pytest.FixtureRequest,
) -> AsyncGenerator[tuple[LocalCommandLineCodeExecutor, str], None]:
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()
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 "hello from powershell!" in result.output
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)