tech-envision commited on
Commit
23550aa
·
unverified ·
2 Parent(s): a42d8c1 2bae1d8

Merge pull request #14 from tech-envision/codex/create-persistent-linux-vm-before-executing-commands

Browse files
Files changed (5) hide show
  1. README.md +4 -2
  2. src/__init__.py +3 -2
  3. src/chat.py +7 -1
  4. src/tools.py +19 -1
  5. src/vm.py +102 -0
README.md CHANGED
@@ -5,8 +5,10 @@ and demonstrates basic tool usage. Chat histories are stored in a local SQLite
5
  database using Peewee. Histories are persisted per user and session so
6
  conversations can be resumed with context. One example tool is included:
7
 
8
- * **execute_terminal** – Executes a shell command in a Linux VM with network
9
- access. Output from ``stdout`` and ``stderr`` is captured and returned.
 
 
10
 
11
  The application now injects a system prompt that instructs the model to chain
12
  multiple tools when required. This prompt ensures the assistant can orchestrate
 
5
  database using Peewee. Histories are persisted per user and session so
6
  conversations can be resumed with context. One example tool is included:
7
 
8
+ * **execute_terminal** – Executes a shell command inside a persistent Linux VM
9
+ with network access. Output from ``stdout`` and ``stderr`` is captured and
10
+ returned. The VM is created when a chat session starts and reused for all
11
+ subsequent tool calls.
12
 
13
  The application now injects a system prompt that instructs the model to chain
14
  multiple tools when required. This prompt ensures the assistant can orchestrate
src/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
  from .chat import ChatSession
2
- from .tools import execute_terminal
 
3
 
4
- __all__ = ["ChatSession", "execute_terminal"]
 
1
  from .chat import ChatSession
2
+ from .tools import execute_terminal, set_vm
3
+ from .vm import LinuxVM
4
 
5
+ __all__ = ["ChatSession", "execute_terminal", "set_vm", "LinuxVM"]
src/chat.py CHANGED
@@ -15,7 +15,8 @@ from .config import (
15
  from .db import Conversation, Message as DBMessage, User, _db, init_db
16
  from .log import get_logger
17
  from .schema import Msg
18
- from .tools import execute_terminal
 
19
 
20
  _LOG = get_logger(__name__)
21
 
@@ -35,13 +36,18 @@ class ChatSession:
35
  self._conversation, _ = Conversation.get_or_create(
36
  user=self._user, session_name=session
37
  )
 
38
  self._messages: List[Msg] = self._load_history()
39
  self._ensure_system_prompt()
40
 
41
  async def __aenter__(self) -> "ChatSession":
 
 
42
  return self
43
 
44
  async def __aexit__(self, exc_type, exc, tb) -> None:
 
 
45
  if not _db.is_closed():
46
  _db.close()
47
 
 
15
  from .db import Conversation, Message as DBMessage, User, _db, init_db
16
  from .log import get_logger
17
  from .schema import Msg
18
+ from .tools import execute_terminal, set_vm
19
+ from .vm import LinuxVM
20
 
21
  _LOG = get_logger(__name__)
22
 
 
36
  self._conversation, _ = Conversation.get_or_create(
37
  user=self._user, session_name=session
38
  )
39
+ self._vm = LinuxVM()
40
  self._messages: List[Msg] = self._load_history()
41
  self._ensure_system_prompt()
42
 
43
  async def __aenter__(self) -> "ChatSession":
44
+ self._vm.start()
45
+ set_vm(self._vm)
46
  return self
47
 
48
  async def __aexit__(self, exc_type, exc, tb) -> None:
49
+ set_vm(None)
50
+ self._vm.stop()
51
  if not _db.is_closed():
52
  _db.close()
53
 
src/tools.py CHANGED
@@ -1,8 +1,20 @@
1
  from __future__ import annotations
2
 
3
- __all__ = ["execute_terminal"]
4
 
5
  import subprocess
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
 
8
  def execute_terminal(command: str, *, timeout: int = 3) -> str:
@@ -14,6 +26,12 @@ def execute_terminal(command: str, *, timeout: int = 3) -> str:
14
  ``stdout`` and ``stderr`` is captured and returned. Commands are killed if
15
  they exceed ``timeout`` seconds.
16
  """
 
 
 
 
 
 
17
  try:
18
  completed = subprocess.run(
19
  command,
 
1
  from __future__ import annotations
2
 
3
+ __all__ = ["execute_terminal", "set_vm"]
4
 
5
  import subprocess
6
+ from typing import Optional
7
+
8
+ from .vm import LinuxVM
9
+
10
+ _VM: Optional[LinuxVM] = None
11
+
12
+
13
+ def set_vm(vm: LinuxVM | None) -> None:
14
+ """Register the VM instance used for command execution."""
15
+
16
+ global _VM
17
+ _VM = vm
18
 
19
 
20
  def execute_terminal(command: str, *, timeout: int = 3) -> str:
 
26
  ``stdout`` and ``stderr`` is captured and returned. Commands are killed if
27
  they exceed ``timeout`` seconds.
28
  """
29
+ if _VM:
30
+ try:
31
+ return _VM.execute(command, timeout=timeout)
32
+ except Exception as exc: # pragma: no cover - unforeseen errors
33
+ return f"Failed to execute command in VM: {exc}"
34
+
35
  try:
36
  completed = subprocess.run(
37
  command,
src/vm.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+ import subprocess
5
+ import uuid
6
+
7
+ from .log import get_logger
8
+
9
+ _LOG = get_logger(__name__)
10
+
11
+
12
+ class LinuxVM:
13
+ """Manage a lightweight Linux VM using Docker."""
14
+
15
+ def __init__(self, image: str = "ubuntu:latest") -> None:
16
+ self._image = image
17
+ self._name = f"chat-vm-{uuid.uuid4().hex[:8]}"
18
+ self._running = False
19
+
20
+ def start(self) -> None:
21
+ """Start the VM if it is not already running."""
22
+ if self._running:
23
+ return
24
+
25
+ try:
26
+ subprocess.run(
27
+ ["docker", "pull", self._image],
28
+ check=False,
29
+ stdout=subprocess.PIPE,
30
+ stderr=subprocess.PIPE,
31
+ text=True,
32
+ )
33
+ subprocess.run(
34
+ [
35
+ "docker",
36
+ "run",
37
+ "-d",
38
+ "--name",
39
+ self._name,
40
+ self._image,
41
+ "sleep",
42
+ "infinity",
43
+ ],
44
+ check=True,
45
+ stdout=subprocess.PIPE,
46
+ stderr=subprocess.PIPE,
47
+ text=True,
48
+ )
49
+ self._running = True
50
+ except Exception as exc: # pragma: no cover - runtime failures
51
+ _LOG.error("Failed to start VM: %s", exc)
52
+ raise RuntimeError(f"Failed to start VM: {exc}") from exc
53
+
54
+ def execute(self, command: str, *, timeout: int = 3) -> str:
55
+ """Execute a command inside the running VM."""
56
+ if not self._running:
57
+ raise RuntimeError("VM is not running")
58
+
59
+ try:
60
+ completed = subprocess.run(
61
+ [
62
+ "docker",
63
+ "exec",
64
+ self._name,
65
+ "bash",
66
+ "-lc",
67
+ command,
68
+ ],
69
+ capture_output=True,
70
+ text=True,
71
+ timeout=timeout,
72
+ )
73
+ except subprocess.TimeoutExpired as exc:
74
+ return f"Command timed out after {timeout}s: {exc.cmd}"
75
+ except Exception as exc: # pragma: no cover - unforeseen errors
76
+ return f"Failed to execute command: {exc}"
77
+
78
+ output = completed.stdout
79
+ if completed.stderr:
80
+ output = f"{output}\n{completed.stderr}" if output else completed.stderr
81
+ return output.strip()
82
+
83
+ def stop(self) -> None:
84
+ """Terminate the VM if running."""
85
+ if not self._running:
86
+ return
87
+
88
+ subprocess.run(
89
+ ["docker", "rm", "-f", self._name],
90
+ check=False,
91
+ stdout=subprocess.PIPE,
92
+ stderr=subprocess.PIPE,
93
+ text=True,
94
+ )
95
+ self._running = False
96
+
97
+ def __enter__(self) -> "LinuxVM":
98
+ self.start()
99
+ return self
100
+
101
+ def __exit__(self, exc_type, exc, tb) -> None:
102
+ self.stop()