Dmitry Beresnev commited on
Commit
bf23d1f
·
1 Parent(s): 4e34289

migrate project to uv, add streamlit ui, etc

Browse files
Files changed (5) hide show
  1. .gitignore +1 -0
  2. Dockerfile +5 -4
  3. app.py +229 -28
  4. pyproject.toml +11 -0
  5. requirements.txt +0 -2
.gitignore CHANGED
@@ -16,6 +16,7 @@ env/
16
  # HF Space build artifacts
17
  *.log
18
  *.lock
 
19
  *.db
20
  *.sqlite
21
  *.cache
 
16
  # HF Space build artifacts
17
  *.log
18
  *.lock
19
+ !uv.lock
20
  *.db
21
  *.sqlite
22
  *.cache
Dockerfile CHANGED
@@ -13,9 +13,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
13
  && (npm install -g openclaw || npm install -g clawdbot) \
14
  && rm -rf /var/lib/apt/lists/*
15
 
16
- # Python dependencies for the Gradio frontend.
17
- COPY requirements.txt /app/requirements.txt
18
- RUN pip install --no-cache-dir -r /app/requirements.txt
 
19
 
20
  COPY . /app
21
 
@@ -28,4 +29,4 @@ RUN mkdir -p /app/vault
28
 
29
  EXPOSE 7860
30
 
31
- CMD ["python", "app.py"]
 
13
  && (npm install -g openclaw || npm install -g clawdbot) \
14
  && rm -rf /var/lib/apt/lists/*
15
 
16
+ # Python dependencies via uv + pyproject.toml.
17
+ RUN pip install --no-cache-dir uv
18
+ COPY pyproject.toml /app/pyproject.toml
19
+ RUN uv sync --no-dev
20
 
21
  COPY . /app
22
 
 
29
 
30
  EXPOSE 7860
31
 
32
+ CMD ["uv", "run", "streamlit", "run", "app.py", "--server.address=0.0.0.0", "--server.port=7860"]
app.py CHANGED
@@ -1,46 +1,247 @@
1
- import gradio as gr
2
- import subprocess
3
- import requests
4
  import os
5
  import shutil
 
 
 
 
 
6
 
7
  HF_PORT = int(os.getenv("PORT", "7860"))
8
  OPENCLAW_PORT = int(os.getenv("OPENCLAW_PORT", "8787"))
9
  VAULT_PATH = os.getenv("VAULT_PATH", "/app/vault")
10
- OPENCLAW_BIN = os.getenv("OPENCLAW_BIN", "openclaw")
11
- OPENCLAW_BIN = OPENCLAW_BIN if shutil.which(OPENCLAW_BIN) else "clawdbot"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- # Run OpenClaw on a different local port than Gradio.
14
- subprocess.Popen(
15
- [
16
- OPENCLAW_BIN,
 
 
17
  "gateway",
18
  "--port",
19
  str(OPENCLAW_PORT),
20
  "--vault-path",
21
  VAULT_PATH,
22
  ]
23
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- def ask_openclaw(question):
26
- # Call your local OpenClaw API
27
  try:
28
- response = requests.post(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  f"http://127.0.0.1:{OPENCLAW_PORT}/ask",
30
- json={"query": question},
31
  timeout=30,
32
  )
33
- response.raise_for_status()
34
- return response.json().get("answer", "No answer")
35
- except Exception as e:
36
- return f"Error: {e}"
37
-
38
- iface = gr.Interface(
39
- fn=ask_openclaw,
40
- inputs="text",
41
- outputs="text",
42
- title="OpenClaw Demo",
43
- description="Ask questions to your notes!"
44
- )
45
-
46
- iface.launch(server_name="0.0.0.0", server_port=HF_PORT)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
 
 
2
  import os
3
  import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ import requests
8
+ import streamlit as st
9
 
10
  HF_PORT = int(os.getenv("PORT", "7860"))
11
  OPENCLAW_PORT = int(os.getenv("OPENCLAW_PORT", "8787"))
12
  VAULT_PATH = os.getenv("VAULT_PATH", "/app/vault")
13
+ OPENCLAW_BIN_ENV = os.getenv("OPENCLAW_BIN", "openclaw")
14
+ CONFIG_PATH = Path(os.getenv("OPENCLAW_CONFIG_PATH", "openclaw.json"))
15
+ ENV_EXAMPLE_PATH = Path("config/openclaw.env.example")
16
+ LOG_MAX_LINES = 300
17
+
18
+
19
+ def resolve_openclaw_bin() -> str | None:
20
+ if shutil.which(OPENCLAW_BIN_ENV):
21
+ return OPENCLAW_BIN_ENV
22
+ if shutil.which("openclaw"):
23
+ return "openclaw"
24
+ if shutil.which("clawdbot"):
25
+ return "clawdbot"
26
+ return None
27
+
28
+
29
+ def init_state() -> None:
30
+ st.session_state.setdefault("gateway_process", None)
31
+ st.session_state.setdefault("gateway_logs", [])
32
+ st.session_state.setdefault("config_editor_text", load_config_text())
33
+ st.session_state.setdefault("auto_started", False)
34
+
35
+
36
+ def load_config_text() -> str:
37
+ if CONFIG_PATH.exists():
38
+ return CONFIG_PATH.read_text(encoding="utf-8")
39
+ return "{}"
40
+
41
+
42
+ def load_config_json() -> dict:
43
+ try:
44
+ return json.loads(load_config_text())
45
+ except json.JSONDecodeError:
46
+ return {}
47
+
48
+
49
+ def gateway_process() -> subprocess.Popen | None:
50
+ proc = st.session_state.get("gateway_process")
51
+ if proc is None:
52
+ return None
53
+ if proc.poll() is not None:
54
+ st.session_state["gateway_process"] = None
55
+ return None
56
+ return proc
57
+
58
+
59
+ def append_logs(lines: list[str]) -> None:
60
+ if not lines:
61
+ return
62
+ st.session_state["gateway_logs"].extend(lines)
63
+ st.session_state["gateway_logs"] = st.session_state["gateway_logs"][-LOG_MAX_LINES:]
64
+
65
+
66
+ def pull_logs() -> None:
67
+ proc = gateway_process()
68
+ if proc is None or proc.stdout is None:
69
+ return
70
+ lines = []
71
+ while True:
72
+ try:
73
+ line = proc.stdout.readline()
74
+ except BlockingIOError:
75
+ break
76
+ if not line:
77
+ break
78
+ lines.append(line.rstrip())
79
+ append_logs(lines)
80
+
81
+
82
+ def start_gateway() -> tuple[bool, str]:
83
+ if gateway_process() is not None:
84
+ return True, "Gateway is already running."
85
 
86
+ binary = resolve_openclaw_bin()
87
+ if binary is None:
88
+ return False, "OpenClaw binary not found (expected `openclaw` or `clawdbot`)."
89
+
90
+ cmd = [
91
+ binary,
92
  "gateway",
93
  "--port",
94
  str(OPENCLAW_PORT),
95
  "--vault-path",
96
  VAULT_PATH,
97
  ]
98
+ proc = subprocess.Popen(
99
+ cmd,
100
+ stdout=subprocess.PIPE,
101
+ stderr=subprocess.STDOUT,
102
+ text=True,
103
+ bufsize=1,
104
+ )
105
+ if proc.stdout is not None:
106
+ os.set_blocking(proc.stdout.fileno(), False)
107
+ st.session_state["gateway_process"] = proc
108
+ append_logs([f"$ {' '.join(cmd)}", "Gateway process started."])
109
+ return True, "Gateway started."
110
+
111
+
112
+ def stop_gateway() -> tuple[bool, str]:
113
+ proc = gateway_process()
114
+ if proc is None:
115
+ return True, "Gateway is not running."
116
 
117
+ proc.terminate()
 
118
  try:
119
+ proc.wait(timeout=10)
120
+ except subprocess.TimeoutExpired:
121
+ proc.kill()
122
+ proc.wait(timeout=5)
123
+ st.session_state["gateway_process"] = None
124
+ append_logs(["Gateway process stopped."])
125
+ return True, "Gateway stopped."
126
+
127
+
128
+ def parse_expected_env_vars(config_data: dict) -> list[str]:
129
+ vars_found: set[str] = set()
130
+ for provider in config_data.get("providers", []):
131
+ base_url = provider.get("base_url")
132
+ if isinstance(base_url, str) and "${LLM_SPACE_URL}" in base_url:
133
+ vars_found.add("LLM_SPACE_URL")
134
+ for tool in config_data.get("tools", {}).values():
135
+ for env_name in tool.get("env", []):
136
+ vars_found.add(env_name)
137
+
138
+ if ENV_EXAMPLE_PATH.exists():
139
+ for line in ENV_EXAMPLE_PATH.read_text(encoding="utf-8").splitlines():
140
+ stripped = line.strip()
141
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
142
+ continue
143
+ vars_found.add(stripped.split("=", 1)[0].strip())
144
+ return sorted(vars_found)
145
+
146
+
147
+ def test_gateway(query: str) -> str:
148
+ try:
149
+ resp = requests.post(
150
  f"http://127.0.0.1:{OPENCLAW_PORT}/ask",
151
+ json={"query": query},
152
  timeout=30,
153
  )
154
+ resp.raise_for_status()
155
+ return json.dumps(resp.json(), indent=2)
156
+ except Exception as exc:
157
+ return f"Gateway request failed: {exc}"
158
+
159
+
160
+ st.set_page_config(page_title="OpenClaw Control Center", layout="wide")
161
+ st.title("OpenClaw Control Center")
162
+ st.caption("Manage gateway runtime, config, environment, and test calls from one UI.")
163
+
164
+ init_state()
165
+ pull_logs()
166
+
167
+ if os.getenv("AUTO_START_GATEWAY", "1") == "1" and not st.session_state["auto_started"]:
168
+ start_gateway()
169
+ st.session_state["auto_started"] = True
170
+
171
+ status_col, action_col = st.columns([2, 3])
172
+ with status_col:
173
+ proc = gateway_process()
174
+ st.metric("Gateway Status", "Running" if proc else "Stopped")
175
+ st.write(f"UI port: `{HF_PORT}`")
176
+ st.write(f"Gateway port: `{OPENCLAW_PORT}`")
177
+ st.write(f"Vault path: `{VAULT_PATH}`")
178
+ st.write(f"Binary: `{resolve_openclaw_bin() or 'not found'}`")
179
+ if proc is not None:
180
+ st.write(f"PID: `{proc.pid}`")
181
+
182
+ with action_col:
183
+ c1, c2, c3, c4 = st.columns(4)
184
+ if c1.button("Start", use_container_width=True):
185
+ ok, msg = start_gateway()
186
+ (st.success if ok else st.error)(msg)
187
+ if c2.button("Stop", use_container_width=True):
188
+ ok, msg = stop_gateway()
189
+ (st.success if ok else st.error)(msg)
190
+ if c3.button("Restart", use_container_width=True):
191
+ stop_gateway()
192
+ ok, msg = start_gateway()
193
+ (st.success if ok else st.error)(msg)
194
+ if c4.button("Refresh", use_container_width=True):
195
+ st.rerun()
196
+
197
+ st.divider()
198
+
199
+ cfg_col, env_col = st.columns(2)
200
+
201
+ with cfg_col:
202
+ st.subheader("Config Editor")
203
+ config_text = st.text_area(
204
+ "openclaw.json",
205
+ value=st.session_state["config_editor_text"],
206
+ height=360,
207
+ )
208
+ st.session_state["config_editor_text"] = config_text
209
+ if st.button("Save Config", use_container_width=True):
210
+ try:
211
+ parsed = json.loads(config_text)
212
+ CONFIG_PATH.write_text(json.dumps(parsed, indent=2) + "\n", encoding="utf-8")
213
+ st.success(f"Saved {CONFIG_PATH}.")
214
+ except json.JSONDecodeError as exc:
215
+ st.error(f"Invalid JSON: {exc}")
216
+
217
+ with env_col:
218
+ st.subheader("Environment Checks")
219
+ config_data = load_config_json()
220
+ expected_vars = parse_expected_env_vars(config_data)
221
+ if not expected_vars:
222
+ st.info("No expected env vars detected.")
223
+ else:
224
+ checks = []
225
+ for key in expected_vars:
226
+ val = os.getenv(key, "")
227
+ checks.append(
228
+ {
229
+ "name": key,
230
+ "status": "set" if val else "missing",
231
+ "preview": (val[:4] + "***") if val else "",
232
+ }
233
+ )
234
+ st.dataframe(checks, use_container_width=True, hide_index=True)
235
+
236
+ st.divider()
237
+
238
+ test_col, logs_col = st.columns([2, 3])
239
+ with test_col:
240
+ st.subheader("Gateway Test")
241
+ test_query = st.text_input("Test prompt", value="Ping from Streamlit")
242
+ if st.button("Send /ask", use_container_width=True):
243
+ st.code(test_gateway(test_query), language="json")
244
+
245
+ with logs_col:
246
+ st.subheader("Gateway Logs")
247
+ st.code("\n".join(st.session_state["gateway_logs"][-LOG_MAX_LINES:]) or "No logs yet.")
pyproject.toml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "openclaw-space"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.11"
5
+ dependencies = [
6
+ "requests==2.32.3",
7
+ "streamlit==1.41.1",
8
+ ]
9
+
10
+ [tool.uv]
11
+ package = false
requirements.txt DELETED
@@ -1,2 +0,0 @@
1
- gradio==4.44.1
2
- requests==2.32.3