|
| 1 | +from hashlib import md5 |
1 | 2 | import os
|
2 | 3 | from pathlib import Path
|
3 | 4 | import re
|
| 5 | +import sys |
4 | 6 | import uuid
|
5 | 7 | import warnings
|
6 |
| -from typing import ClassVar, List, Optional, Union |
7 |
| -from pydantic import Field |
| 8 | +from typing import ClassVar, List, Union |
8 | 9 |
|
9 | 10 | from ..agentchat.agent import LLMAgent
|
10 |
| -from ..code_utils import execute_code |
11 |
| -from .base import CodeBlock, CodeExecutor, CodeExtractor, CodeResult |
| 11 | +from ..code_utils import TIMEOUT_MSG, WIN32, _cmd, execute_code |
| 12 | +from .base import CodeBlock, CodeExecutor, CodeExtractor, CommandLineCodeResult |
12 | 13 | from .markdown_code_extractor import MarkdownCodeExtractor
|
13 | 14 |
|
14 |
| -__all__ = ( |
15 |
| - "LocalCommandLineCodeExecutor", |
16 |
| - "CommandLineCodeResult", |
17 |
| -) |
| 15 | +from .utils import _get_file_name_from_content |
18 | 16 |
|
| 17 | +import subprocess |
19 | 18 |
|
20 |
| -class CommandLineCodeResult(CodeResult): |
21 |
| - """(Experimental) A code result class for command line code executor.""" |
22 |
| - |
23 |
| - code_file: Optional[str] = Field( |
24 |
| - default=None, |
25 |
| - description="The file that the executed code block was saved to.", |
26 |
| - ) |
| 19 | +__all__ = ("LocalCommandLineCodeExecutor",) |
27 | 20 |
|
28 | 21 |
|
29 | 22 | class LocalCommandLineCodeExecutor(CodeExecutor):
|
| 23 | + SUPPORTED_LANGUAGES: ClassVar[List[str]] = ["bash", "shell", "sh", "pwsh", "powershell", "ps1", "python"] |
| 24 | + |
30 | 25 | def __init__(
|
31 | 26 | self,
|
32 | 27 | timeout: int = 60,
|
@@ -113,41 +108,59 @@ def execute_code_blocks(self, code_blocks: List[CodeBlock]) -> CommandLineCodeRe
|
113 | 108 | Returns:
|
114 | 109 | CommandLineCodeResult: The result of the code execution."""
|
115 | 110 | logs_all = ""
|
| 111 | + file_names = [] |
116 | 112 | for code_block in code_blocks:
|
117 | 113 | lang, code = code_block.language, code_block.code
|
| 114 | + lang = lang.lower() |
118 | 115 |
|
119 | 116 | LocalCommandLineCodeExecutor.sanitize_command(lang, code)
|
120 |
| - filename_uuid = uuid.uuid4().hex |
121 |
| - filename = None |
122 |
| - if lang in ["bash", "shell", "sh", "pwsh", "powershell", "ps1"]: |
123 |
| - filename = f"{filename_uuid}.{lang}" |
124 |
| - exitcode, logs, _ = execute_code( |
125 |
| - code=code, |
126 |
| - lang=lang, |
127 |
| - timeout=self._timeout, |
128 |
| - work_dir=str(self._work_dir), |
129 |
| - filename=filename, |
130 |
| - use_docker=False, |
131 |
| - ) |
132 |
| - elif lang in ["python", "Python"]: |
133 |
| - filename = f"{filename_uuid}.py" |
134 |
| - exitcode, logs, _ = execute_code( |
135 |
| - code=code, |
136 |
| - lang="python", |
137 |
| - timeout=self._timeout, |
138 |
| - work_dir=str(self._work_dir), |
139 |
| - filename=filename, |
140 |
| - use_docker=False, |
141 |
| - ) |
142 |
| - else: |
| 117 | + |
| 118 | + if WIN32 and lang in ["sh", "shell"]: |
| 119 | + lang = "ps1" |
| 120 | + |
| 121 | + if lang not in self.SUPPORTED_LANGUAGES: |
143 | 122 | # In case the language is not supported, we return an error message.
|
144 |
| - exitcode, logs, _ = (1, f"unknown language {lang}", None) |
145 |
| - logs_all += "\n" + logs |
| 123 | + exitcode = 1 |
| 124 | + logs_all += "\n" + f"unknown language {lang}" |
| 125 | + break |
| 126 | + |
| 127 | + try: |
| 128 | + # Check if there is a filename comment |
| 129 | + filename = _get_file_name_from_content(code, self._work_dir) |
| 130 | + except ValueError: |
| 131 | + return CommandLineCodeResult(exit_code=1, output="Filename is not in the workspace") |
| 132 | + |
| 133 | + if filename is None: |
| 134 | + # create a file with an automatically generated name |
| 135 | + code_hash = md5(code.encode()).hexdigest() |
| 136 | + filename = f"tmp_code_{code_hash}.{'py' if lang.startswith('python') else lang}" |
| 137 | + |
| 138 | + written_file = (self._work_dir / filename).resolve() |
| 139 | + written_file.open("w", encoding="utf-8").write(code) |
| 140 | + file_names.append(written_file) |
| 141 | + |
| 142 | + program = sys.executable if lang.startswith("python") else _cmd(lang) |
| 143 | + cmd = [program, str(written_file.absolute())] |
| 144 | + |
| 145 | + try: |
| 146 | + result = subprocess.run( |
| 147 | + cmd, cwd=self._work_dir, capture_output=True, text=True, timeout=float(self._timeout) |
| 148 | + ) |
| 149 | + except subprocess.TimeoutExpired: |
| 150 | + logs_all += "\n" + TIMEOUT_MSG |
| 151 | + # Same exit code as the timeout command on linux. |
| 152 | + exitcode = 124 |
| 153 | + break |
| 154 | + |
| 155 | + logs_all += result.stderr |
| 156 | + logs_all += result.stdout |
| 157 | + exitcode = result.returncode |
| 158 | + |
146 | 159 | if exitcode != 0:
|
147 | 160 | break
|
148 | 161 |
|
149 |
| - code_filename = str(self._work_dir / filename) if filename is not None else None |
150 |
| - return CommandLineCodeResult(exit_code=exitcode, output=logs_all, code_file=code_filename) |
| 162 | + code_file = str(file_names[0]) if len(file_names) > 0 else None |
| 163 | + return CommandLineCodeResult(exit_code=exitcode, output=logs_all, code_file=code_file) |
151 | 164 |
|
152 | 165 | def restart(self) -> None:
|
153 | 166 | """(Experimental) Restart the code executor."""
|
|
0 commit comments