tech-envision commited on
Commit
c8f7ad8
·
1 Parent(s): a8e6ea9

Run terminal commands in background

Browse files
Files changed (3) hide show
  1. README.md +3 -2
  2. src/tools.py +11 -16
  3. src/vm.py +36 -13
README.md CHANGED
@@ -9,8 +9,9 @@ conversations can be resumed with context. One example tool is included:
9
  with network access. Use it to read uploaded documents under ``/data``, fetch
10
  web content via tools like ``curl`` or run any other commands. The assistant
11
  must invoke this tool to search online when unsure about a response. Output
12
- from ``stdout`` and ``stderr`` is captured and returned. Commands run
13
- asynchronously so the assistant can continue responding while they execute.
 
14
  The VM is created when a chat session starts and reused for all subsequent
15
  tool calls.
16
 
 
9
  with network access. Use it to read uploaded documents under ``/data``, fetch
10
  web content via tools like ``curl`` or run any other commands. The assistant
11
  must invoke this tool to search online when unsure about a response. Output
12
+ from ``stdout`` and ``stderr`` is captured when available. Commands are
13
+ launched in the background with no timeout so the assistant can continue
14
+ responding while they execute.
15
  The VM is created when a chat session starts and reused for all subsequent
16
  tool calls.
17
 
src/tools.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
  __all__ = ["execute_terminal", "execute_terminal_async", "set_vm"]
4
 
5
  import subprocess
 
6
  from typing import Optional
7
  import asyncio
8
 
@@ -26,38 +27,32 @@ def execute_terminal(command: str) -> str:
26
  The assistant must call this tool to search the internet whenever unsure
27
  about any detail.
28
 
29
- The command is executed with network access enabled. Output from both
30
- ``stdout`` and ``stderr`` is captured and returned. Commands are killed if
31
- they exceed 30 seconds.
32
  """
33
- timeout = 30
34
  if not command:
35
  return "No command provided."
36
 
37
  if _VM:
38
  try:
39
- return _VM.execute(command, timeout=timeout)
40
  except Exception as exc: # pragma: no cover - unforeseen errors
41
  return f"Failed to execute command in VM: {exc}"
42
 
43
  try:
44
- completed = subprocess.run(
45
  command,
46
  shell=True,
47
- capture_output=True,
48
- text=True,
49
- timeout=timeout,
 
50
  )
51
- except subprocess.TimeoutExpired as exc:
52
- return f"Command timed out after {timeout}s: {exc.cmd}"
53
  except Exception as exc: # pragma: no cover - unforeseen errors
54
  return f"Failed to execute command: {exc}"
55
 
56
- output = completed.stdout
57
- if completed.stderr:
58
- output = f"{output}\n{completed.stderr}" if output else completed.stderr
59
- return output.strip()
60
-
61
 
62
  async def execute_terminal_async(command: str) -> str:
63
  """Asynchronously execute a shell command."""
 
3
  __all__ = ["execute_terminal", "execute_terminal_async", "set_vm"]
4
 
5
  import subprocess
6
+ import os
7
  from typing import Optional
8
  import asyncio
9
 
 
27
  The assistant must call this tool to search the internet whenever unsure
28
  about any detail.
29
 
30
+ The command is executed with network access enabled. It runs in the
31
+ background without a timeout so the assistant can continue responding
32
+ while the command executes.
33
  """
 
34
  if not command:
35
  return "No command provided."
36
 
37
  if _VM:
38
  try:
39
+ return _VM.execute(command, detach=True)
40
  except Exception as exc: # pragma: no cover - unforeseen errors
41
  return f"Failed to execute command in VM: {exc}"
42
 
43
  try:
44
+ subprocess.Popen(
45
  command,
46
  shell=True,
47
+ stdout=subprocess.DEVNULL,
48
+ stderr=subprocess.DEVNULL,
49
+ start_new_session=True,
50
+ env=os.environ.copy(),
51
  )
52
+ return "Command started in background."
 
53
  except Exception as exc: # pragma: no cover - unforeseen errors
54
  return f"Failed to execute command: {exc}"
55
 
 
 
 
 
 
56
 
57
  async def execute_terminal_async(command: str) -> str:
58
  """Asynchronously execute a shell command."""
src/vm.py CHANGED
@@ -67,24 +67,45 @@ class LinuxVM:
67
  _LOG.error("Failed to start VM: %s", exc)
68
  raise RuntimeError(f"Failed to start VM: {exc}") from exc
69
 
70
- def execute(self, command: str, *, timeout: int = 3) -> str:
71
- """Execute a command inside the running VM."""
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  if not self._running:
73
  raise RuntimeError("VM is not running")
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  try:
76
  completed = subprocess.run(
77
- [
78
- "docker",
79
- "exec",
80
- self._name,
81
- "bash",
82
- "-lc",
83
- command,
84
- ],
85
  capture_output=True,
86
  text=True,
87
- timeout=timeout,
88
  )
89
  except subprocess.TimeoutExpired as exc:
90
  return f"Command timed out after {timeout}s: {exc.cmd}"
@@ -96,10 +117,12 @@ class LinuxVM:
96
  output = f"{output}\n{completed.stderr}" if output else completed.stderr
97
  return output.strip()
98
 
99
- async def execute_async(self, command: str, *, timeout: int = 3) -> str:
 
 
100
  """Asynchronously execute ``command`` inside the running VM."""
101
  loop = asyncio.get_running_loop()
102
- func = partial(self.execute, command, timeout=timeout)
103
  return await loop.run_in_executor(None, func)
104
 
105
  def stop(self) -> None:
 
67
  _LOG.error("Failed to start VM: %s", exc)
68
  raise RuntimeError(f"Failed to start VM: {exc}") from exc
69
 
70
+ def execute(
71
+ self, command: str, *, timeout: int = 3, detach: bool = False
72
+ ) -> str:
73
+ """Execute a command inside the running VM.
74
+
75
+ Parameters
76
+ ----------
77
+ command:
78
+ The shell command to run inside the container.
79
+ timeout:
80
+ Maximum time in seconds to wait for completion. Ignored when
81
+ ``detach`` is ``True``.
82
+ detach:
83
+ Run the command in the background without waiting for it to finish.
84
+ """
85
  if not self._running:
86
  raise RuntimeError("VM is not running")
87
 
88
+ cmd = [
89
+ "docker",
90
+ "exec",
91
+ ]
92
+ if detach:
93
+ cmd.append("-d")
94
+ cmd.extend(
95
+ [
96
+ self._name,
97
+ "bash",
98
+ "-lc",
99
+ command,
100
+ ]
101
+ )
102
+
103
  try:
104
  completed = subprocess.run(
105
+ cmd,
 
 
 
 
 
 
 
106
  capture_output=True,
107
  text=True,
108
+ timeout=None if detach else timeout,
109
  )
110
  except subprocess.TimeoutExpired as exc:
111
  return f"Command timed out after {timeout}s: {exc.cmd}"
 
117
  output = f"{output}\n{completed.stderr}" if output else completed.stderr
118
  return output.strip()
119
 
120
+ async def execute_async(
121
+ self, command: str, *, timeout: int = 3, detach: bool = False
122
+ ) -> str:
123
  """Asynchronously execute ``command`` inside the running VM."""
124
  loop = asyncio.get_running_loop()
125
+ func = partial(self.execute, command, timeout=timeout, detach=detach)
126
  return await loop.run_in_executor(None, func)
127
 
128
  def stop(self) -> None: