tech-envision commited on
Commit
ca5f649
Β·
1 Parent(s): 6d08a3a

feat: allow VM filesystem browsing in gradio UI

Browse files
Files changed (2) hide show
  1. README.md +1 -0
  2. gradio_app.py +48 -46
README.md CHANGED
@@ -7,6 +7,7 @@
7
  - **Persistent chat history** – conversations are stored in `chat.db` per user and session so they can be resumed later.
8
  - **Tool execution** – a built-in `execute_terminal` tool runs commands inside a Docker-based VM using `docker exec -i`. Network access is enabled and both stdout and stderr are captured (up to 10,000 characters). The VM is reused across chats when `PERSIST_VMS=1` so installed packages remain available.
9
  - **System prompts** – every request includes a system prompt that guides the assistant to plan tool usage, verify results and avoid unnecessary jargon.
 
10
 
11
  ## Environment Variables
12
 
 
7
  - **Persistent chat history** – conversations are stored in `chat.db` per user and session so they can be resumed later.
8
  - **Tool execution** – a built-in `execute_terminal` tool runs commands inside a Docker-based VM using `docker exec -i`. Network access is enabled and both stdout and stderr are captured (up to 10,000 characters). The VM is reused across chats when `PERSIST_VMS=1` so installed packages remain available.
9
  - **System prompts** – every request includes a system prompt that guides the assistant to plan tool usage, verify results and avoid unnecessary jargon.
10
+ - **Gradio interface** – a web UI in `gradio_app.py` lets you chat and browse the VM file system. The Files tab now allows navigating any directory inside the container.
11
 
12
  ## Environment Variables
13
 
gradio_app.py CHANGED
@@ -1,6 +1,7 @@
1
  import asyncio
2
- import shutil
3
- from pathlib import Path
 
4
 
5
  import gradio as gr
6
  from gradio.oauth import attach_oauth, OAuthToken
@@ -8,7 +9,6 @@ from huggingface_hub import HfApi
8
 
9
  from src.team import TeamChatSession
10
  from src.db import list_sessions_info
11
- from src.config import UPLOAD_DIR
12
 
13
  # Store active chat sessions
14
  _SESSIONS: dict[tuple[str, str], TeamChatSession] = {}
@@ -32,16 +32,18 @@ async def _get_chat(user: str, session: str) -> TeamChatSession:
32
  return chat
33
 
34
 
35
- def _vm_host_path(user: str, vm_path: str) -> Path:
36
- rel = Path(vm_path).relative_to("/data")
37
- base = (Path(UPLOAD_DIR) / user).resolve()
38
- target = (base / rel).resolve()
39
- if not target.is_relative_to(base):
40
- raise ValueError("Invalid path")
41
- return target
42
 
43
 
44
- async def send_message(message: str, history: list[tuple[str, str]], session: str, token: OAuthToken):
 
 
45
  user = _username(token)
46
  chat = await _get_chat(user, session)
47
  history = history or []
@@ -60,48 +62,48 @@ def load_sessions(token: OAuthToken):
60
  return gr.update(choices=names or ["default"], value=value), table
61
 
62
 
63
- def list_dir(path: str, token: OAuthToken):
64
  user = _username(token)
65
- target = _vm_host_path(user, path)
66
- if not target.exists() or not target.is_dir():
 
67
  return []
68
  entries = []
69
- for entry in sorted(target.iterdir()):
70
- entries.append({"name": entry.name, "is_dir": entry.is_dir()})
 
 
 
 
 
71
  return entries
72
 
73
 
74
- def read_file(path: str, token: OAuthToken):
75
  user = _username(token)
76
- target = _vm_host_path(user, path)
77
- if not target.exists():
78
- return "File not found"
79
- if target.is_dir():
80
- return "Path is a directory"
81
- try:
82
- return target.read_text()
83
- except UnicodeDecodeError:
84
- return "Binary file not supported"
85
-
86
-
87
- def save_file(path: str, content: str, token: OAuthToken):
88
  user = _username(token)
89
- target = _vm_host_path(user, path)
90
- target.parent.mkdir(parents=True, exist_ok=True)
91
- target.write_text(content)
 
 
 
92
  return "Saved"
93
 
94
 
95
- def delete_path(path: str, token: OAuthToken):
96
  user = _username(token)
97
- target = _vm_host_path(user, path)
98
- if target.is_dir():
99
- shutil.rmtree(target)
100
- elif target.exists():
101
- target.unlink()
102
- else:
103
- return "File not found"
104
- return "Deleted"
105
 
106
 
107
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
@@ -117,7 +119,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
117
  send = gr.Button("Send")
118
 
119
  with gr.Tab("Files"):
120
- dir_path = gr.Textbox(label="Directory", value="/data")
121
  list_btn = gr.Button("List")
122
  table = gr.Dataframe(headers=["name", "is_dir"], datatype=["str", "bool"])
123
  file_path = gr.Textbox(label="File Path")
@@ -132,10 +134,10 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
132
  inputs=[msg, chatbox, session_dd],
133
  outputs=chatbox,
134
  )
135
- list_btn.click(list_dir, inputs=dir_path, outputs=table)
136
- load_btn.click(read_file, inputs=file_path, outputs=content)
137
- save_btn.click(save_file, inputs=[file_path, content], outputs=content)
138
- del_btn.click(delete_path, inputs=file_path, outputs=content)
139
  send_click.then(lambda: "", None, msg)
140
 
141
 
 
1
  import asyncio
2
+ import base64
3
+ import json
4
+ import shlex
5
 
6
  import gradio as gr
7
  from gradio.oauth import attach_oauth, OAuthToken
 
9
 
10
  from src.team import TeamChatSession
11
  from src.db import list_sessions_info
 
12
 
13
  # Store active chat sessions
14
  _SESSIONS: dict[tuple[str, str], TeamChatSession] = {}
 
32
  return chat
33
 
34
 
35
+ async def _vm_execute(user: str, session: str, command: str) -> str:
36
+ """Execute ``command`` inside the user's VM and return output."""
37
+ chat = await _get_chat(user, session)
38
+ vm = getattr(chat.senior, "_vm", None)
39
+ if vm is None:
40
+ raise RuntimeError("VM not running")
41
+ return await vm.execute_async(command, timeout=5)
42
 
43
 
44
+ async def send_message(
45
+ message: str, history: list[tuple[str, str]], session: str, token: OAuthToken
46
+ ):
47
  user = _username(token)
48
  chat = await _get_chat(user, session)
49
  history = history or []
 
62
  return gr.update(choices=names or ["default"], value=value), table
63
 
64
 
65
+ async def list_dir(path: str, session: str, token: OAuthToken):
66
  user = _username(token)
67
+ cmd = f"ls -1ap {shlex.quote(path)}"
68
+ output = await _vm_execute(user, session, cmd)
69
+ if output.startswith("ls:"):
70
  return []
71
  entries = []
72
+ for line in output.splitlines():
73
+ line = line.strip()
74
+ if not line or line in (".", ".."):
75
+ continue
76
+ is_dir = line.endswith("/")
77
+ name = line[:-1] if is_dir else line
78
+ entries.append({"name": name, "is_dir": is_dir})
79
  return entries
80
 
81
 
82
+ async def read_file(path: str, session: str, token: OAuthToken):
83
  user = _username(token)
84
+ cmd = f"cat {shlex.quote(path)}"
85
+ return await _vm_execute(user, session, cmd)
86
+
87
+
88
+ async def save_file(path: str, content: str, session: str, token: OAuthToken):
 
 
 
 
 
 
 
89
  user = _username(token)
90
+ encoded = base64.b64encode(content.encode()).decode()
91
+ cmd = (
92
+ f"python -c 'import base64,os; "
93
+ f'open({json.dumps(path)}, "wb").write(base64.b64decode({json.dumps(encoded)}))\''
94
+ )
95
+ await _vm_execute(user, session, cmd)
96
  return "Saved"
97
 
98
 
99
+ async def delete_path(path: str, session: str, token: OAuthToken):
100
  user = _username(token)
101
+ cmd = (
102
+ f"bash -c 'if [ -d {shlex.quote(path)} ]; then rm -rf {shlex.quote(path)} && echo Deleted; "
103
+ f"elif [ -e {shlex.quote(path)} ]; then rm -f {shlex.quote(path)} && echo Deleted; "
104
+ f"else echo File not found; fi'"
105
+ )
106
+ return await _vm_execute(user, session, cmd)
 
 
107
 
108
 
109
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
 
119
  send = gr.Button("Send")
120
 
121
  with gr.Tab("Files"):
122
+ dir_path = gr.Textbox(label="Directory", value="/")
123
  list_btn = gr.Button("List")
124
  table = gr.Dataframe(headers=["name", "is_dir"], datatype=["str", "bool"])
125
  file_path = gr.Textbox(label="File Path")
 
134
  inputs=[msg, chatbox, session_dd],
135
  outputs=chatbox,
136
  )
137
+ list_btn.click(list_dir, inputs=[dir_path, session_dd], outputs=table)
138
+ load_btn.click(read_file, inputs=[file_path, session_dd], outputs=content)
139
+ save_btn.click(save_file, inputs=[file_path, content, session_dd], outputs=content)
140
+ del_btn.click(delete_path, inputs=[file_path, session_dd], outputs=content)
141
  send_click.then(lambda: "", None, msg)
142
 
143