Jibbalit commited on
Commit
f4bfc02
·
0 Parent(s):

Set up HF Spaces external bridge with Docker dev mode.

Browse files
.env ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # External bridge endpoint (local machine side)
2
+ LOCAL_BRIDGE_URL=ws://localhost:8765
3
+
4
+ # Optional controls
5
+ SECURITY_LEVEL=high
6
+ LOG_LEVEL=INFO
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__/
2
+ *.pyc
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ portaudio19-dev \
7
+ ffmpeg \
8
+ wget \
9
+ curl \
10
+ git \
11
+ build-essential \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ COPY requirements.txt /app/requirements.txt
15
+ RUN pip install --no-cache-dir -r /app/requirements.txt
16
+
17
+ COPY . /app
18
+ RUN mkdir -p /app/logs /app/temp /app/data
19
+
20
+ EXPOSE 7860
21
+
22
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=20s --retries=3 \
23
+ CMD curl -f http://localhost:7860/ || exit 1
24
+
25
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
Dockerfile.dev ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1
4
+ ENV DEV_MODE=true
5
+ ENV SSH_ENABLED=true
6
+ ENV DISPLAY=:99
7
+
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ openssh-server \
10
+ sudo \
11
+ curl \
12
+ wget \
13
+ git \
14
+ vim \
15
+ xvfb \
16
+ x11vnc \
17
+ ffmpeg \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ RUN mkdir /var/run/sshd \
21
+ && useradd -m -s /bin/bash devuser \
22
+ && echo "devuser:devpass" | chpasswd \
23
+ && echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \
24
+ && chmod 440 /etc/sudoers.d/devuser \
25
+ && echo "Port 22" >> /etc/ssh/sshd_config \
26
+ && echo "PermitRootLogin no" >> /etc/ssh/sshd_config \
27
+ && echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
28
+
29
+ WORKDIR /app
30
+
31
+ COPY requirements.txt /app/requirements.txt
32
+ RUN pip install --no-cache-dir -r /app/requirements.txt \
33
+ && pip install --no-cache-dir jupyter jupyterlab ipython black flake8 pytest
34
+
35
+ COPY . /app
36
+ RUN mkdir -p /app/logs /app/temp /app/data /home/devuser/.ssh \
37
+ && chown -R devuser:devuser /home/devuser /app
38
+
39
+ EXPOSE 22 7860 5900 8888
40
+
41
+ COPY dev-start.sh /dev-start.sh
42
+ RUN chmod +x /dev-start.sh
43
+
44
+ CMD ["/dev-start.sh"]
Dockerfile.external ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1
4
+ ENV DISPLAY=:99
5
+
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ curl \
8
+ wget \
9
+ ffmpeg \
10
+ xvfb \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ WORKDIR /app
14
+
15
+ COPY requirements.txt /app/requirements.txt
16
+ RUN pip install --no-cache-dir -r /app/requirements.txt
17
+
18
+ COPY . /app
19
+
20
+ RUN mkdir -p /logs /downloads /audio
21
+
22
+ EXPOSE 8501 8765 8000
23
+
24
+ COPY start.sh /start.sh
25
+ RUN chmod +x /start.sh
26
+
27
+ CMD ["/start.sh"]
README.md ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Abliterated Agency Hub
2
+
3
+ Streamlit + Docker setup for Hugging Face Spaces with an external bridge and SSH-enabled development mode.
4
+
5
+ ## Project Structure
6
+
7
+ ```text
8
+ abliterated-agency/
9
+ ├── Dockerfile
10
+ ├── Dockerfile.dev
11
+ ├── Dockerfile.external
12
+ ├── docker-compose.yml
13
+ ├── docker-compose.dev.yml
14
+ ├── app.py
15
+ ├── bridge_client.py
16
+ ├── security_manager.py
17
+ ├── voice_handler.py
18
+ ├── model_configs.json
19
+ ├── security_config.json
20
+ ├── requirements.txt
21
+ ├── .env
22
+ ├── external_server.py
23
+ ├── dev-start.sh
24
+ ├── start.sh
25
+ └── ssh-setup.sh
26
+ ```
27
+
28
+ ## HF Spaces Deploy (One Shot)
29
+
30
+ 1. Use `app.py` and `requirements.txt` at repo root.
31
+ 2. Push to your Space:
32
+
33
+ ```bash
34
+ git init
35
+ git add .
36
+ git commit -m "Abliterated agency hub setup"
37
+ git remote add origin https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
38
+ git push -u origin main
39
+ ```
40
+
41
+ For token auth:
42
+
43
+ ```bash
44
+ git remote set-url origin https://USER:TOKEN@huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
45
+ git push -u origin main
46
+ ```
47
+
48
+ ## Docker Pile Breakdown
49
+
50
+ - `Dockerfile`
51
+ Primary runtime image for Streamlit on port `7860` (HF-compatible).
52
+ - `docker-compose.yml`
53
+ Standard local container run for the same app.
54
+ - `Dockerfile.dev`
55
+ Dev container with SSH, VNC, and Jupyter installed.
56
+ - `docker-compose.dev.yml`
57
+ Development mode stack with local source mount and extra dev ports.
58
+ - `Dockerfile.external`
59
+ Alternate external bridge image (kept for compatibility with earlier flow).
60
+
61
+ ## Standard Docker Run
62
+
63
+ ```bash
64
+ docker compose up --build
65
+ ```
66
+
67
+ App URL: `http://localhost:7860`
68
+
69
+ ## Dev Mode + SSH
70
+
71
+ Start dev stack:
72
+
73
+ ```bash
74
+ docker compose -f docker-compose.dev.yml up --build -d
75
+ ```
76
+
77
+ Access points:
78
+
79
+ - Streamlit: `http://localhost:7860`
80
+ - SSH: `ssh devuser@localhost -p 2222` (password: `devpass`)
81
+ - VNC: `localhost:5900`
82
+ - Jupyter: `http://localhost:8888`
83
+
84
+ Quick SSH startup helper:
85
+
86
+ ```bash
87
+ bash ssh-setup.sh
88
+ ```
89
+
90
+ ## SSH Notes
91
+
92
+ Docker dev image includes:
93
+
94
+ - `openssh-server`
95
+ - `sudo`
96
+ - user `devuser` / `devpass`
97
+ - `PermitRootLogin no`
98
+ - `PasswordAuthentication yes`
99
+
100
+ X11 forwarding:
101
+
102
+ ```bash
103
+ ssh -X devuser@localhost -p 2222
104
+ ```
105
+
106
+ Tunnel from container to local bridge:
107
+
108
+ ```bash
109
+ ssh -L 8765:localhost:8765 user@host
110
+ ```
111
+
112
+ ## Streamlit Dev Mode Flags
113
+
114
+ Dev startup uses:
115
+
116
+ ```bash
117
+ streamlit run app.py \
118
+ --server.runOnSave=true \
119
+ --server.headless=false \
120
+ --server.address=0.0.0.0 \
121
+ --server.port=7860
122
+ ```
123
+
124
+ ## External Bridge Server (Your Machine)
125
+
126
+ Run external bridge:
127
+
128
+ ```bash
129
+ pip install websockets
130
+ python external_server.py
131
+ ```
132
+
133
+ Default bridge endpoint: `ws://localhost:8765`
134
+
135
+ ## Security Website Access
136
+
137
+ Defaults now allow:
138
+
139
+ - `youtube.com`
140
+ - `instagram.com`
141
+ - `tiktok.com`
142
+
143
+ Configured in `security_config.json` and exposed in the Streamlit sidebar controls.
app.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ import streamlit as st
9
+
10
+ from bridge_client import BridgeClient
11
+ from security_manager import SecurityManager
12
+ from voice_handler import VoiceHandler
13
+
14
+ st.set_page_config(
15
+ page_title="Abliterated Agency Hub",
16
+ page_icon="🤖",
17
+ layout="wide",
18
+ initial_sidebar_state="expanded",
19
+ )
20
+
21
+
22
+ @st.cache_data
23
+ def load_model_configs() -> dict:
24
+ path = Path("model_configs.json")
25
+ if not path.exists():
26
+ return {}
27
+ return json.loads(path.read_text(encoding="utf-8"))
28
+
29
+
30
+ def init_state() -> None:
31
+ if "messages" not in st.session_state:
32
+ st.session_state.messages = []
33
+ if "security_state" not in st.session_state:
34
+ st.session_state.security_state = {
35
+ "browser_access": False,
36
+ "file_system_access": False,
37
+ "cursor_access": False,
38
+ "voice_enabled": True,
39
+ "current_voice_model": "kimi_k2_5_q8",
40
+ }
41
+ if "bridge_connected" not in st.session_state:
42
+ st.session_state.bridge_connected = False
43
+ if "voice_mode" not in st.session_state:
44
+ st.session_state.voice_mode = "text"
45
+ if "bridge_client" not in st.session_state:
46
+ st.session_state.bridge_client = BridgeClient()
47
+ if "security_manager" not in st.session_state:
48
+ st.session_state.security_manager = SecurityManager()
49
+ if "voice_handler" not in st.session_state:
50
+ st.session_state.voice_handler = VoiceHandler()
51
+
52
+
53
+ def main() -> None:
54
+ init_state()
55
+ model_configs = load_model_configs()
56
+
57
+ with st.sidebar:
58
+ st.title("Security Control Panel")
59
+ st.subheader("Access Controls")
60
+ st.session_state.security_state["browser_access"] = st.toggle(
61
+ "Browser Access", value=st.session_state.security_state["browser_access"]
62
+ )
63
+ st.session_state.security_state["file_system_access"] = st.toggle(
64
+ "File System Access", value=st.session_state.security_state["file_system_access"]
65
+ )
66
+ st.session_state.security_state["cursor_access"] = st.toggle(
67
+ "Cursor Control", value=st.session_state.security_state["cursor_access"]
68
+ )
69
+
70
+ st.subheader("Website Gate")
71
+ blocked_sites = st.text_area(
72
+ "Blocked Websites",
73
+ value="",
74
+ )
75
+ allowed_sites = st.text_area(
76
+ "Allowed Websites",
77
+ value="google.com,github.com,huggingface.co,youtube.com,instagram.com,tiktok.com",
78
+ )
79
+
80
+ sm = st.session_state.security_manager
81
+ sm.blocked_domains = {x.strip() for x in blocked_sites.split(",") if x.strip()}
82
+ sm.allowed_domains = {x.strip() for x in allowed_sites.split(",") if x.strip()}
83
+ sm.save_config()
84
+
85
+ st.subheader("Voice Settings")
86
+ st.session_state.voice_mode = st.selectbox(
87
+ "Voice Mode",
88
+ options=["text", "voice", "hybrid"],
89
+ index=["text", "voice", "hybrid"].index(st.session_state.voice_mode),
90
+ )
91
+ st.session_state.security_state["current_voice_model"] = st.selectbox(
92
+ "Voice Model",
93
+ options=["kimi_k2_5_q8", "whisper_speech", "granite_speech"],
94
+ index=["kimi_k2_5_q8", "whisper_speech", "granite_speech"].index(
95
+ st.session_state.security_state["current_voice_model"]
96
+ ),
97
+ )
98
+
99
+ st.subheader("Bridge Connection")
100
+ if st.button("Connect to Local Bridge"):
101
+ st.session_state.bridge_connected = st.session_state.bridge_client.connect()
102
+ if st.session_state.bridge_connected:
103
+ st.success("Bridge connected.")
104
+ else:
105
+ st.error("Bridge unavailable.")
106
+
107
+ st.subheader("Model Status")
108
+ for cfg in model_configs.values():
109
+ online = cfg.get("available", False)
110
+ st.text(f"{cfg.get('name', 'unknown')}: {'Online' if online else 'Offline'}")
111
+
112
+ st.title("Abliterated Agency Hub")
113
+ st.markdown("### AI coordinator with external bridge and security gates")
114
+
115
+ for message in st.session_state.messages:
116
+ with st.chat_message(message["role"]):
117
+ st.write(message["content"])
118
+ st.caption(message["timestamp"])
119
+
120
+ if st.session_state.voice_mode != "text":
121
+ if st.button("Start Voice Input"):
122
+ heard = st.session_state.voice_handler.listen()
123
+ if heard:
124
+ st.session_state.messages.append(
125
+ {
126
+ "role": "user",
127
+ "content": heard,
128
+ "timestamp": datetime.now().strftime("%H:%M:%S"),
129
+ }
130
+ )
131
+ st.rerun()
132
+
133
+ prompt = st.chat_input("Message your agent...")
134
+ if prompt:
135
+ st.session_state.messages.append(
136
+ {"role": "user", "content": prompt, "timestamp": datetime.now().strftime("%H:%M:%S")}
137
+ )
138
+
139
+ if "video" in prompt.lower() or "generate" in prompt.lower():
140
+ model_name = "minimax_video"
141
+ elif "image" in prompt.lower() or "picture" in prompt.lower():
142
+ model_name = "phi4_analysis"
143
+ else:
144
+ model_name = "kimi_k2_5_q8"
145
+
146
+ with st.spinner("Processing..."):
147
+ response = st.session_state.bridge_client.send_request(
148
+ model=model_name,
149
+ prompt=prompt,
150
+ security_context=st.session_state.security_state,
151
+ )
152
+
153
+ st.session_state.messages.append(
154
+ {
155
+ "role": "assistant",
156
+ "content": response,
157
+ "timestamp": datetime.now().strftime("%H:%M:%S"),
158
+ }
159
+ )
160
+
161
+ if st.session_state.voice_mode in {"voice", "hybrid"}:
162
+ st.session_state.voice_handler.speak(response)
163
+
164
+ st.rerun()
165
+
166
+ st.divider()
167
+ c1, c2, c3, c4 = st.columns(4)
168
+ c1.metric("Bridge", "Connected" if st.session_state.bridge_connected else "Disconnected")
169
+ c2.metric("Security", "Custom")
170
+ c3.metric("Voice Mode", st.session_state.voice_mode.title())
171
+ c4.metric(
172
+ "Active Models",
173
+ len([m for m in model_configs.values() if m.get("available", False)]),
174
+ )
175
+
176
+
177
+ if __name__ == "__main__":
178
+ main()
bridge_client.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ from datetime import datetime
7
+ from typing import Any, Dict, Optional
8
+
9
+ import websockets
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class BridgeClient:
15
+ def __init__(self, bridge_url: str = "ws://localhost:8765") -> None:
16
+ self.bridge_url = bridge_url
17
+ self.connected = False
18
+
19
+ async def _connect_async(self) -> bool:
20
+ """Probe external bridge by opening/closing a websocket."""
21
+ try:
22
+ async with websockets.connect(self.bridge_url, open_timeout=3):
23
+ self.connected = True
24
+ return True
25
+ except Exception as exc:
26
+ logger.warning("Bridge connect failed: %s", exc)
27
+ self.connected = False
28
+ return False
29
+
30
+ async def _send_request_async(
31
+ self, model: str, prompt: str, security_context: Dict[str, Any]
32
+ ) -> str:
33
+ payload = {
34
+ "model": model,
35
+ "prompt": prompt,
36
+ "security_context": security_context,
37
+ "timestamp": datetime.utcnow().isoformat(),
38
+ }
39
+
40
+ try:
41
+ async with websockets.connect(self.bridge_url, open_timeout=4) as ws:
42
+ await ws.send(json.dumps(payload))
43
+ raw = await ws.recv()
44
+ data = json.loads(raw)
45
+ self.connected = True
46
+ return data.get("response", "No response received.")
47
+ except Exception as exc:
48
+ self.connected = False
49
+ return (
50
+ f"[Bridge unavailable] {exc}. "
51
+ "No external system response was received."
52
+ )
53
+
54
+ def connect(self) -> bool:
55
+ return asyncio.run(self._connect_async())
56
+
57
+ def send_request(
58
+ self, model: str, prompt: str, security_context: Dict[str, Any]
59
+ ) -> str:
60
+ return asyncio.run(self._send_request_async(model, prompt, security_context))
dev-start.sh ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ echo "Starting development services..."
5
+
6
+ service ssh start
7
+
8
+ Xvfb :99 -screen 0 1280x800x24 >/tmp/xvfb.log 2>&1 &
9
+ x11vnc -display :99 -forever -nopw -rfbport 5900 >/tmp/x11vnc.log 2>&1 &
10
+
11
+ jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --allow-root --ServerApp.token='' >/tmp/jupyter.log 2>&1 &
12
+
13
+ cd /app
14
+ streamlit run app.py \
15
+ --server.port=7860 \
16
+ --server.address=0.0.0.0 \
17
+ --server.runOnSave=true \
18
+ --server.headless=false >/tmp/streamlit.log 2>&1 &
19
+
20
+ tail -f /tmp/streamlit.log
docker-compose.dev.yml ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.8"
2
+
3
+ services:
4
+ agency-dev:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile.dev
8
+ container_name: abliterated-agency-dev
9
+ ports:
10
+ - "7860:7860"
11
+ - "2222:22"
12
+ - "5900:5900"
13
+ - "8888:8888"
14
+ volumes:
15
+ - ./:/app
16
+ - ./logs:/app/logs
17
+ - ./ssh:/root/.ssh
18
+ - ./vscode:/root/.vscode
19
+ environment:
20
+ - DEVELOPMENT_MODE=true
21
+ - SSH_ENABLE=true
22
+ - VNC_ENABLE=true
23
+ - JUPYTER_ENABLE=true
24
+ - DISPLAY=:99
25
+ - PYTHONUNBUFFERED=1
26
+ env_file:
27
+ - .env
28
+ stdin_open: true
29
+ tty: true
30
+ networks:
31
+ - dev-net
32
+ restart: unless-stopped
33
+
34
+ networks:
35
+ dev-net:
36
+ driver: bridge
docker-compose.yml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.8"
2
+
3
+ services:
4
+ agency-hub:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ container_name: abliterated-agency
9
+ ports:
10
+ - "7860:7860"
11
+ env_file:
12
+ - .env
13
+ volumes:
14
+ - ./logs:/app/logs
15
+ - ./data:/app/data
16
+ - ./temp:/app/temp
17
+ restart: unless-stopped
external_server.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """External system server running on your own machine."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import json
8
+ import logging
9
+
10
+ import websockets
11
+
12
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - external: %(message)s")
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def handle_client(websocket):
17
+ async for message in websocket:
18
+ data = json.loads(message)
19
+ logger.info("Received: %s", data)
20
+ response = {
21
+ "type": "response",
22
+ "command": data.get("command"),
23
+ "success": True,
24
+ "data": {"message": "External system processed command"},
25
+ }
26
+ await websocket.send(json.dumps(response))
27
+
28
+
29
+ async def main() -> None:
30
+ async with websockets.serve(handle_client, "0.0.0.0", 8765):
31
+ logger.info("External system server started on ws://0.0.0.0:8765")
32
+ await asyncio.Future()
33
+
34
+
35
+ if __name__ == "__main__":
36
+ asyncio.run(main())
model_configs.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kimi_k2_5_q8": {
3
+ "name": "Kimi-K2.5-Q8",
4
+ "available": true,
5
+ "role": "general_planner"
6
+ },
7
+ "phi4_analysis": {
8
+ "name": "Phi-4 Analysis",
9
+ "available": true,
10
+ "role": "analysis"
11
+ },
12
+ "minimax_video": {
13
+ "name": "MiniMax Video",
14
+ "available": false,
15
+ "role": "video_generation"
16
+ }
17
+ }
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.41.0
2
+ streamlit-webrtc>=0.47.0
3
+ requests>=2.31.0
4
+ websockets>=12.0
5
+ aiohttp>=3.9.0
6
+ numpy>=1.24.0
7
+ pandas>=2.0.0
8
+ pytrends>=4.9.0
9
+ GoogleNews>=1.6.0
10
+ feedparser>=6.0.0
11
+ python-dotenv>=1.0.0
12
+ pydantic>=2.5.0
13
+ cryptography>=41.0.0
14
+ httpx>=0.25.0
15
+ psutil>=5.9.0
16
+ SpeechRecognition>=3.10.0
17
+ pyttsx3>=2.90
security_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "blocked_domains": [],
3
+ "allowed_domains": [
4
+ "google.com",
5
+ "github.com",
6
+ "huggingface.co",
7
+ "youtube.com",
8
+ "instagram.com",
9
+ "tiktok.com"
10
+ ],
11
+ "policies": {
12
+ "default_deny_when_allowlist_set": true
13
+ }
14
+ }
security_manager.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Set
6
+ from urllib.parse import urlparse
7
+
8
+
9
+ class SecurityManager:
10
+ def __init__(self, config_file: str = "security_config.json") -> None:
11
+ self.config_file = Path(config_file)
12
+ self.blocked_domains: Set[str] = set()
13
+ self.allowed_domains: Set[str] = set()
14
+ self.policies: Dict[str, Any] = {}
15
+ self.load_config()
16
+
17
+ def load_config(self) -> None:
18
+ if self.config_file.exists():
19
+ data = json.loads(self.config_file.read_text(encoding="utf-8"))
20
+ self.blocked_domains = set(data.get("blocked_domains", []))
21
+ self.allowed_domains = set(data.get("allowed_domains", []))
22
+ self.policies = data.get("policies", {})
23
+ return
24
+
25
+ self.blocked_domains = set()
26
+ self.allowed_domains = {
27
+ "google.com",
28
+ "github.com",
29
+ "huggingface.co",
30
+ "youtube.com",
31
+ "instagram.com",
32
+ "tiktok.com",
33
+ }
34
+ self.policies = {"default_deny_when_allowlist_set": True}
35
+ self.save_config()
36
+
37
+ def save_config(self) -> None:
38
+ data = {
39
+ "blocked_domains": sorted(self.blocked_domains),
40
+ "allowed_domains": sorted(self.allowed_domains),
41
+ "policies": self.policies,
42
+ }
43
+ self.config_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
44
+
45
+ def is_website_allowed(self, url: str) -> bool:
46
+ try:
47
+ host = urlparse(url).netloc.lower().replace("www.", "")
48
+ if any(d in host for d in self.blocked_domains):
49
+ return False
50
+ if self.allowed_domains:
51
+ return any(d in host for d in self.allowed_domains)
52
+ return True
53
+ except Exception:
54
+ return False
55
+
56
+ def validate_action(self, action: str, context: Dict[str, Any]) -> bool:
57
+ """Simple policy gate; expandable."""
58
+ if action == "browser_navigate":
59
+ url = context.get("url", "")
60
+ return self.is_website_allowed(url)
61
+ return True
62
+
63
+ def get_security_report(self) -> Dict[str, Any]:
64
+ return {
65
+ "blocked_domains_count": len(self.blocked_domains),
66
+ "allowed_domains_count": len(self.allowed_domains),
67
+ "policies_active": bool(self.policies),
68
+ "security_level": "high",
69
+ }
ssh-setup.sh ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ CONTAINER_NAME="${1:-abliterated-agency-dev}"
5
+ docker exec -it "$CONTAINER_NAME" /etc/init.d/ssh start
6
+ echo "SSH ready on port 2222 (host mapping)."
7
+ echo "Login: devuser/devpass"
start.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ echo "Starting HF Spaces external bridge..."
5
+ cd /app
6
+
7
+ if [ -f "app.py" ]; then
8
+ exec streamlit run app.py --server.port=8501 --server.address=0.0.0.0
9
+ else
10
+ exec streamlit run streamlit_app.py --server.port=8501 --server.address=0.0.0.0
11
+ fi
streamlit_app.py ADDED
@@ -0,0 +1,638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Apex OmniAgent — Streamlit UI
3
+ ==============================
4
+ A Streamlit-based interface that mirrors the React UI layout:
5
+ - Top toolbar with security toggles and status indicators
6
+ - Three-pane layout: File Explorer | Workspace Canvas | Tools Panel
7
+ - Bottom terminal pane with log viewer and command input
8
+
9
+ Connects to the same Python gateway API at http://localhost:7860.
10
+
11
+ Usage:
12
+ streamlit run streamlit_app.py
13
+ streamlit run streamlit_app.py -- --gateway http://localhost:7860
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import sys
21
+
22
+ import requests
23
+ import streamlit as st
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Configuration
27
+ # ---------------------------------------------------------------------------
28
+
29
+ GATEWAY_URL = "http://localhost:7860"
30
+
31
+ # Parse --gateway flag when launched via: streamlit run streamlit_app.py -- --gateway URL
32
+ _parser = argparse.ArgumentParser(add_help=False)
33
+ _parser.add_argument("--gateway", default=GATEWAY_URL)
34
+ _args, _ = _parser.parse_known_args(sys.argv[1:])
35
+ GATEWAY_URL = _args.gateway.rstrip("/")
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Page config
39
+ # ---------------------------------------------------------------------------
40
+
41
+ st.set_page_config(
42
+ page_title="Apex OmniAgent",
43
+ page_icon="⬡",
44
+ layout="wide",
45
+ initial_sidebar_state="collapsed",
46
+ )
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Custom CSS — matches the React/Tailwind dark theme
50
+ # ---------------------------------------------------------------------------
51
+ # Color tokens (from tailwind.config.js):
52
+ # apex-bg: #0d0d0d
53
+ # apex-panel: #141420
54
+ # apex-border: #2a2a4a
55
+ # apex-accent: #7060e0
56
+ # apex-accent-light: #a090ff
57
+
58
+ _CUSTOM_CSS = """
59
+ <style>
60
+ /* ── Root theme ───────────────────────────────────────────────── */
61
+ :root {
62
+ color-scheme: dark;
63
+ }
64
+ [data-testid="stAppViewContainer"],
65
+ [data-testid="stApp"],
66
+ [data-testid="stHeader"],
67
+ section[data-testid="stSidebar"] {
68
+ background-color: #0d0d0d !important;
69
+ color: #e0e0f0 !important;
70
+ }
71
+ [data-testid="stAppViewBlockContainer"] {
72
+ padding-top: 0.5rem !important;
73
+ }
74
+
75
+ /* ── Panel cards ──────────────────────────────────────────────── */
76
+ .panel-card {
77
+ background: #141420;
78
+ border: 1px solid #2a2a4a;
79
+ border-radius: 6px;
80
+ padding: 0;
81
+ overflow: hidden;
82
+ height: 100%;
83
+ }
84
+ .panel-header {
85
+ padding: 8px 12px;
86
+ font-size: 11px;
87
+ font-weight: 600;
88
+ text-transform: uppercase;
89
+ letter-spacing: 0.12em;
90
+ color: #a090ff;
91
+ border-bottom: 1px solid #2a2a4a;
92
+ }
93
+ .panel-body {
94
+ padding: 8px 12px;
95
+ }
96
+
97
+ /* ── Toolbar ──────────────────────────────────────────────────── */
98
+ .toolbar {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 16px;
102
+ padding: 8px 16px;
103
+ background: #0a0a1a;
104
+ border-bottom: 1px solid #2a2a4a;
105
+ margin-bottom: 8px;
106
+ border-radius: 6px;
107
+ }
108
+ .toolbar-title {
109
+ color: #a090ff;
110
+ font-weight: 700;
111
+ font-size: 13px;
112
+ letter-spacing: 0.05em;
113
+ white-space: nowrap;
114
+ }
115
+ .toolbar-divider {
116
+ width: 1px;
117
+ height: 20px;
118
+ background: #2a2a4a;
119
+ }
120
+ .toolbar-label {
121
+ font-size: 11px;
122
+ color: #6b7280;
123
+ margin-right: 4px;
124
+ }
125
+ .status-dot {
126
+ display: inline-block;
127
+ width: 8px;
128
+ height: 8px;
129
+ border-radius: 50%;
130
+ margin-right: 4px;
131
+ }
132
+ .status-dot.green { background: #22c55e; }
133
+ .status-dot.blue { background: #60a5fa; }
134
+ .status-dot.gray { background: #4b5563; }
135
+ .status-dot.red { background: #ef4444; }
136
+ .status-text {
137
+ font-size: 11px;
138
+ color: #6b7280;
139
+ }
140
+
141
+ /* ── File tree ────────────────────────────────────────────────── */
142
+ .file-tree-folder {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 4px;
146
+ padding: 3px 0;
147
+ font-size: 12px;
148
+ color: #d1d5db;
149
+ cursor: pointer;
150
+ user-select: none;
151
+ }
152
+ .file-tree-file {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 6px;
156
+ padding: 3px 0;
157
+ font-size: 12px;
158
+ color: #9ca3af;
159
+ }
160
+ .file-tree-file:hover {
161
+ color: #ffffff;
162
+ }
163
+
164
+ /* ── Node palette ─────────────────────────────────────────────── */
165
+ .node-card {
166
+ display: flex;
167
+ align-items: flex-start;
168
+ gap: 8px;
169
+ padding: 8px;
170
+ border: 1px solid #2a2a4a;
171
+ border-radius: 6px;
172
+ margin-bottom: 6px;
173
+ background: transparent;
174
+ }
175
+ .node-card:hover {
176
+ background: rgba(255,255,255,0.03);
177
+ }
178
+ .node-icon {
179
+ font-size: 16px;
180
+ flex-shrink: 0;
181
+ }
182
+ .node-label {
183
+ font-size: 12px;
184
+ font-weight: 500;
185
+ color: #d1d5db;
186
+ }
187
+ .node-desc {
188
+ font-size: 10px;
189
+ color: #6b7280;
190
+ }
191
+
192
+ /* ── Connection status ────────────────────────────────────────── */
193
+ .conn-item {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 8px;
197
+ padding: 3px 0;
198
+ font-size: 12px;
199
+ color: #9ca3af;
200
+ }
201
+
202
+ /* ── Settings row ─────────────────────────────────────────────── */
203
+ .settings-row {
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: space-between;
207
+ padding: 3px 0;
208
+ font-size: 12px;
209
+ color: #9ca3af;
210
+ }
211
+ .settings-value {
212
+ font-size: 10px;
213
+ }
214
+ .settings-value.enabled { color: #22c55e; }
215
+ .settings-value.disabled { color: #6b7280; font-style: italic; }
216
+
217
+ /* ── Terminal ─────────────────────────────────────────────────── */
218
+ .terminal-wrap {
219
+ background: rgba(0,0,0,0.9);
220
+ border: 1px solid #2a2a4a;
221
+ border-radius: 6px 6px 0 0;
222
+ overflow: hidden;
223
+ }
224
+ .terminal-header {
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: space-between;
228
+ padding: 4px 12px;
229
+ border-bottom: 1px solid #2a2a4a;
230
+ }
231
+ .terminal-title {
232
+ font-family: monospace;
233
+ font-size: 10px;
234
+ color: #a090ff;
235
+ letter-spacing: 0.1em;
236
+ }
237
+ .terminal-body {
238
+ padding: 8px 12px;
239
+ max-height: 180px;
240
+ overflow-y: auto;
241
+ font-family: monospace;
242
+ font-size: 10px;
243
+ line-height: 1.7;
244
+ }
245
+ .log-normal { color: #9ca3af; }
246
+ .log-error { color: #f87171; }
247
+ .log-warning { color: #facc15; }
248
+ .log-cmd { color: #86efac; }
249
+
250
+ /* ── Canvas workspace (Graphviz) ──────────────────────────────── */
251
+ .canvas-wrap {
252
+ background: #0d0d14;
253
+ border: 1px solid #2a2a4a;
254
+ border-radius: 6px;
255
+ padding: 12px;
256
+ min-height: 360px;
257
+ display: flex;
258
+ flex-direction: column;
259
+ }
260
+
261
+ /* ── Kill switch button ───────────────────────────────────────── */
262
+ .kill-btn {
263
+ font-size: 10px;
264
+ padding: 2px 8px;
265
+ border-radius: 4px;
266
+ border: 1px solid #991b1b;
267
+ color: #f87171;
268
+ background: transparent;
269
+ cursor: pointer;
270
+ }
271
+ .kill-btn:hover { background: rgba(127,29,29,0.3); }
272
+ .kill-btn.armed {
273
+ border-color: #ef4444;
274
+ background: rgba(127,29,29,0.4);
275
+ color: #fca5a5;
276
+ cursor: not-allowed;
277
+ }
278
+
279
+ /* ── Toggle pills (matching React toggle-on / toggle-off) ───── */
280
+ .toggle-pill {
281
+ display: inline-flex;
282
+ align-items: center;
283
+ gap: 6px;
284
+ padding: 4px 8px;
285
+ border-radius: 4px;
286
+ font-size: 11px;
287
+ cursor: pointer;
288
+ user-select: none;
289
+ }
290
+ .toggle-pill.on { background: #7060e0; color: #fff; }
291
+ .toggle-pill.off { background: #141420; border: 1px solid #2a2a4a; color: #9ca3af; }
292
+
293
+ /* ── Utility ──────────────────────────────────────────────────── */
294
+ .spacer { flex: 1; }
295
+
296
+ /* ── Hide Streamlit default padding / branding ────────────────── */
297
+ #MainMenu { visibility: hidden; }
298
+ footer { visibility: hidden; }
299
+ header { visibility: hidden !important; }
300
+ [data-testid="stDecoration"] { display: none; }
301
+ </style>
302
+ """
303
+
304
+ st.markdown(_CUSTOM_CSS, unsafe_allow_html=True)
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # Session state defaults
309
+ # ---------------------------------------------------------------------------
310
+
311
+ if "logs" not in st.session_state:
312
+ st.session_state.logs = [
313
+ "[gateway] Starting Apex OmniAgent gateway on :7860",
314
+ "[gateway] PolicyEnforcer loaded — suggest_only=True",
315
+ "[gateway] HotwordDetector ready (Porcupine)",
316
+ "[capture] ScreenCapture initialized (mss)",
317
+ "[audit] Audit log → ~/.apex-agent/audit.log",
318
+ "[info] Waiting for hotword…",
319
+ ]
320
+
321
+ if "kill_switch" not in st.session_state:
322
+ st.session_state.kill_switch = False
323
+
324
+ if "suggest_only" not in st.session_state:
325
+ st.session_state.suggest_only = True
326
+
327
+ if "allow_execute" not in st.session_state:
328
+ st.session_state.allow_execute = False
329
+
330
+ if "allow_network" not in st.session_state:
331
+ st.session_state.allow_network = False
332
+
333
+
334
+ # ---------------------------------------------------------------------------
335
+ # Gateway helpers
336
+ # ---------------------------------------------------------------------------
337
+
338
+ def _gw_get(path: str) -> dict | None:
339
+ """GET from gateway; returns parsed JSON or None on error."""
340
+ try:
341
+ r = requests.get(f"{GATEWAY_URL}{path}", timeout=5)
342
+ return r.json()
343
+ except Exception:
344
+ return None
345
+
346
+
347
+ def _gw_post(path: str, body: dict | None = None) -> dict | None:
348
+ """POST to gateway; returns parsed JSON or None on error."""
349
+ try:
350
+ r = requests.post(f"{GATEWAY_URL}{path}", json=body, timeout=10)
351
+ return r.json()
352
+ except Exception:
353
+ return None
354
+
355
+
356
+ def _check_gateway() -> bool:
357
+ data = _gw_get("/health")
358
+ return data is not None and data.get("status") == "ok"
359
+
360
+
361
+ # ---------------------------------------------------------------------------
362
+ # Toolbar
363
+ # ---------------------------------------------------------------------------
364
+
365
+ gateway_ok = _check_gateway()
366
+
367
+ _toolbar_toggles_html = ""
368
+ for key, label, state_key in [
369
+ ("suggest_only", "Suggest Only", "suggest_only"),
370
+ ("allow_execute", "Allow Execute", "allow_execute"),
371
+ ("allow_network", "Allow Network", "allow_network"),
372
+ ]:
373
+ on = st.session_state[state_key]
374
+ cls = "on" if on else "off"
375
+ dot = "background:#fff" if on else "background:#4b5563"
376
+ _toolbar_toggles_html += (
377
+ f'<span class="toggle-pill {cls}">'
378
+ f'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;{dot}"></span>'
379
+ f"{label}</span> "
380
+ )
381
+
382
+ gw_dot = "green" if gateway_ok else "gray"
383
+
384
+ st.markdown(
385
+ f"""<div class="toolbar">
386
+ <div class="toolbar-title">⬡ Apex OmniAgent</div>
387
+ <div class="toolbar-divider"></div>
388
+ <span class="toolbar-label">Security:</span>
389
+ {_toolbar_toggles_html}
390
+ <div class="spacer"></div>
391
+ <span class="status-text"><span class="status-dot {gw_dot}"></span>Gateway</span>
392
+ <span class="status-text"><span class="status-dot blue"></span>HuggingFace</span>
393
+ </div>""",
394
+ unsafe_allow_html=True,
395
+ )
396
+
397
+ # Interactive toggle controls (using Streamlit columns for actual interactivity)
398
+ tog_cols = st.columns([1, 1, 1, 4])
399
+ with tog_cols[0]:
400
+ st.session_state.suggest_only = st.checkbox(
401
+ "Suggest Only", value=st.session_state.suggest_only, label_visibility="collapsed"
402
+ )
403
+ with tog_cols[1]:
404
+ st.session_state.allow_execute = st.checkbox(
405
+ "Allow Execute", value=st.session_state.allow_execute, label_visibility="collapsed"
406
+ )
407
+ with tog_cols[2]:
408
+ st.session_state.allow_network = st.checkbox(
409
+ "Allow Network", value=st.session_state.allow_network, label_visibility="collapsed"
410
+ )
411
+
412
+
413
+ # ---------------------------------------------------------------------------
414
+ # Three-pane layout
415
+ # ---------------------------------------------------------------------------
416
+
417
+ col_left, col_center, col_right = st.columns([2, 5, 2], gap="small")
418
+
419
+ # ── Left: File Explorer ───────────────────────────────────────────────────────
420
+
421
+ PROJECT_TREE = {
422
+ "Project": {
423
+ "icon": "📁",
424
+ "children": {
425
+ "pipeline.flow": "🔷",
426
+ "settings.json": "📄",
427
+ },
428
+ },
429
+ "Nodes": {
430
+ "icon": "📁",
431
+ "children": {
432
+ "AudioInputNode.tsx": "⚛️",
433
+ "ScreenCaptureNode.tsx": "⚛️",
434
+ "LLMQueryNode.tsx": "⚛️",
435
+ "PythonRunnerNode.tsx": "⚛️",
436
+ },
437
+ },
438
+ "Configs": {
439
+ "icon": "📁",
440
+ "children": {
441
+ "policy.json": "📄",
442
+ ".env.example": "🔑",
443
+ },
444
+ },
445
+ "Logs": {
446
+ "icon": "📁",
447
+ "children": {
448
+ "audit.log": "📋",
449
+ },
450
+ },
451
+ }
452
+
453
+ with col_left:
454
+ st.markdown('<div class="panel-card">', unsafe_allow_html=True)
455
+ st.markdown('<div class="panel-header">Explorer</div>', unsafe_allow_html=True)
456
+ st.markdown('<div class="panel-body">', unsafe_allow_html=True)
457
+ for folder, info in PROJECT_TREE.items():
458
+ with st.expander(f"📁 {folder}", expanded=True):
459
+ for fname, ficon in info["children"].items():
460
+ st.markdown(
461
+ f'<div class="file-tree-file">{ficon} {fname}</div>',
462
+ unsafe_allow_html=True,
463
+ )
464
+ st.markdown("</div></div>", unsafe_allow_html=True)
465
+
466
+
467
+ # ── Center: Workspace Canvas ──────────────────────────────────────────────────
468
+
469
+ with col_center:
470
+ st.markdown('<div class="panel-card">', unsafe_allow_html=True)
471
+ st.markdown('<div class="panel-header">Workspace Canvas</div>', unsafe_allow_html=True)
472
+ st.markdown('<div class="canvas-wrap">', unsafe_allow_html=True)
473
+
474
+ # Workflow graph using Graphviz (mirrors the React Flow default pipeline)
475
+ st.graphviz_chart(
476
+ """
477
+ digraph pipeline {
478
+ bgcolor="#0d0d14"
479
+ graph [rankdir=LR, pad="0.3", nodesep="0.6", ranksep="1.0"]
480
+ node [
481
+ style="filled,rounded"
482
+ shape=box
483
+ fontname="Segoe UI"
484
+ fontsize=11
485
+ fontcolor="#e0e0f0"
486
+ color="#2a2a4a"
487
+ fillcolor="#141420"
488
+ penwidth=1.2
489
+ ]
490
+ edge [
491
+ color="#7060e0"
492
+ penwidth=1.5
493
+ arrowsize=0.7
494
+ ]
495
+
496
+ audio [label="🎙 Audio Input" fillcolor="#1a1028"]
497
+ screen [label="📸 Screen Capture" fillcolor="#0f1528"]
498
+ llm [label="🤖 LLM Query" fillcolor="#18102a"]
499
+ runner [label="🐍 Python Runner" fillcolor="#0f1a14"]
500
+
501
+ audio -> llm
502
+ screen -> llm
503
+ llm -> runner
504
+ }
505
+ """,
506
+ use_container_width=True,
507
+ )
508
+
509
+ st.markdown("</div></div>", unsafe_allow_html=True)
510
+
511
+
512
+ # ── Right: Tools Panel ────────────────────────────────────────────────────────
513
+
514
+ NODE_PALETTE = [
515
+ ("🎙", "Audio Input", "Hotword & PTT capture"),
516
+ ("📸", "Screen Capture", "Full-screen capture via mss"),
517
+ ("🤖", "LLM Query", "HuggingFace Inference API"),
518
+ ("🐍", "Python Runner", "Execute gateway script"),
519
+ ]
520
+
521
+ CONNECTIONS = [
522
+ ("Python Gateway", "green" if gateway_ok else "gray"),
523
+ ("HuggingFace API", "gray"),
524
+ ]
525
+
526
+ with col_right:
527
+ # ── Nodes palette ──
528
+ st.markdown('<div class="panel-card">', unsafe_allow_html=True)
529
+ st.markdown('<div class="panel-header">Nodes</div>', unsafe_allow_html=True)
530
+ st.markdown('<div class="panel-body">', unsafe_allow_html=True)
531
+ for icon, label, desc in NODE_PALETTE:
532
+ st.markdown(
533
+ f"""<div class="node-card">
534
+ <span class="node-icon">{icon}</span>
535
+ <div><div class="node-label">{label}</div><div class="node-desc">{desc}</div></div>
536
+ </div>""",
537
+ unsafe_allow_html=True,
538
+ )
539
+ st.markdown("</div>", unsafe_allow_html=True)
540
+
541
+ # ── Settings ──
542
+ st.markdown('<div class="panel-header">Settings</div>', unsafe_allow_html=True)
543
+ st.markdown('<div class="panel-body">', unsafe_allow_html=True)
544
+ st.markdown(
545
+ """<div class="settings-row"><span>TTS Output</span>
546
+ <span class="settings-value disabled">Silent (default)</span></div>""",
547
+ unsafe_allow_html=True,
548
+ )
549
+ st.markdown(
550
+ """<div class="settings-row"><span>Audit Log</span>
551
+ <span class="settings-value enabled">Enabled</span></div>""",
552
+ unsafe_allow_html=True,
553
+ )
554
+
555
+ # Kill-switch
556
+ armed = st.session_state.kill_switch
557
+ if st.button(
558
+ "🔴 Armed" if armed else "Arm Kill-switch",
559
+ disabled=armed,
560
+ key="kill_btn",
561
+ ):
562
+ result = _gw_post("/kill")
563
+ st.session_state.kill_switch = True
564
+ st.session_state.logs.append("[gateway] 🔴 KILL SWITCH ACTIVATED")
565
+ st.rerun()
566
+
567
+ st.markdown("</div>", unsafe_allow_html=True)
568
+
569
+ # ── Connections ──
570
+ st.markdown('<div class="panel-header">Connections</div>', unsafe_allow_html=True)
571
+ st.markdown('<div class="panel-body">', unsafe_allow_html=True)
572
+ for name, color in CONNECTIONS:
573
+ st.markdown(
574
+ f'<div class="conn-item"><span class="status-dot {color}"></span>{name}</div>',
575
+ unsafe_allow_html=True,
576
+ )
577
+ st.markdown("</div></div>", unsafe_allow_html=True)
578
+
579
+
580
+ # ---------------------------------------------------------------------------
581
+ # Terminal Pane
582
+ # ---------------------------------------------------------------------------
583
+
584
+ st.markdown("---", unsafe_allow_html=True)
585
+ st.markdown('<div class="terminal-wrap">', unsafe_allow_html=True)
586
+ st.markdown(
587
+ '<div class="terminal-header">'
588
+ '<span class="terminal-title">⬡ Terminal</span>'
589
+ "</div>",
590
+ unsafe_allow_html=True,
591
+ )
592
+
593
+ # Log output
594
+ log_html = ""
595
+ for line in st.session_state.logs[-50:]:
596
+ if "[error]" in line:
597
+ cls = "log-error"
598
+ elif "[warn]" in line:
599
+ cls = "log-warning"
600
+ elif line.startswith(">"):
601
+ cls = "log-cmd"
602
+ else:
603
+ cls = "log-normal"
604
+ escaped = line.replace("<", "&lt;").replace(">", "&gt;")
605
+ log_html += f'<div class="{cls}">{escaped}</div>'
606
+
607
+ st.markdown(
608
+ f'<div class="terminal-body">{log_html}</div>',
609
+ unsafe_allow_html=True,
610
+ )
611
+ st.markdown("</div>", unsafe_allow_html=True)
612
+
613
+ # Command input
614
+ with st.form("cmd_form", clear_on_submit=True):
615
+ cmd_cols = st.columns([10, 1])
616
+ with cmd_cols[0]:
617
+ cmd = st.text_input(
618
+ "command",
619
+ placeholder="Enter command…",
620
+ label_visibility="collapsed",
621
+ )
622
+ with cmd_cols[1]:
623
+ submitted = st.form_submit_button("▶")
624
+
625
+ if submitted and cmd.strip():
626
+ st.session_state.logs.append(f"> {cmd.strip()}")
627
+ result = _gw_post("/command", {"command": cmd.strip(), "payload": {}})
628
+ if result is None:
629
+ st.session_state.logs.append(
630
+ "[error] Gateway unreachable"
631
+ )
632
+ elif result.get("error"):
633
+ st.session_state.logs.append(f"[error] {result['error']}")
634
+ else:
635
+ st.session_state.logs.append(
636
+ f"[gateway] {json.dumps(result)}"
637
+ )
638
+ st.rerun()
voice_handler.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class VoiceHandler:
10
+ """Graceful voice helper with optional dependencies."""
11
+
12
+ def __init__(self) -> None:
13
+ self._recognizer = None
14
+ self._microphone = None
15
+ self._tts_engine = None
16
+ self._init_optional_backends()
17
+
18
+ def _init_optional_backends(self) -> None:
19
+ try:
20
+ import speech_recognition as sr
21
+
22
+ self._recognizer = sr.Recognizer()
23
+ self._microphone = sr.Microphone()
24
+ except Exception as exc:
25
+ logger.warning("SpeechRecognition unavailable: %s", exc)
26
+
27
+ try:
28
+ import pyttsx3
29
+
30
+ self._tts_engine = pyttsx3.init()
31
+ except Exception as exc:
32
+ logger.warning("TTS unavailable: %s", exc)
33
+
34
+ def listen(self, timeout: int = 4, phrase_time_limit: int = 8) -> Optional[str]:
35
+ if not self._recognizer or not self._microphone:
36
+ return None
37
+ try:
38
+ with self._microphone as source:
39
+ self._recognizer.adjust_for_ambient_noise(source, duration=0.5)
40
+ audio = self._recognizer.listen(
41
+ source, timeout=timeout, phrase_time_limit=phrase_time_limit
42
+ )
43
+ return self._recognizer.recognize_google(audio).strip()
44
+ except Exception as exc:
45
+ logger.warning("Voice listen failed: %s", exc)
46
+ return None
47
+
48
+ def speak(self, text: str) -> bool:
49
+ if not self._tts_engine:
50
+ return False
51
+ try:
52
+ self._tts_engine.say(text)
53
+ self._tts_engine.runAndWait()
54
+ return True
55
+ except Exception as exc:
56
+ logger.warning("Voice speak failed: %s", exc)
57
+ return False