Реализация полноценного прокси для работы Playground UI в HuggingFace Space
Browse files
app.py
CHANGED
@@ -6,6 +6,10 @@ import time
|
|
6 |
from pathlib import Path
|
7 |
import signal
|
8 |
import shutil
|
|
|
|
|
|
|
|
|
9 |
|
10 |
def main():
|
11 |
processes = []
|
@@ -23,29 +27,33 @@ def main():
|
|
23 |
print(f"ERROR: Playground directory not found at {playground_dir}", file=sys.stderr)
|
24 |
return 1
|
25 |
|
26 |
-
# Создаем директорию для логов
|
27 |
-
|
|
|
|
|
|
|
28 |
|
29 |
# Запускаем API сервер
|
30 |
print("Starting TEN-Agent API server on port 8080...")
|
31 |
api_server_env = os.environ.copy()
|
32 |
-
api_server_env["LOG_PATH"] =
|
33 |
api_server_env["LOG_STDOUT"] = "true"
|
34 |
api_server_process = subprocess.Popen([str(api_binary)], env=api_server_env)
|
35 |
processes.append(api_server_process)
|
36 |
|
37 |
# Даем время API серверу запуститься
|
38 |
-
time.sleep(
|
39 |
|
40 |
-
# Запускаем Playground UI
|
41 |
print("Starting Playground UI on port 3000...")
|
42 |
playground_env = os.environ.copy()
|
43 |
playground_env["AGENT_SERVER_URL"] = "http://localhost:8080" # Подключаемся к локальному API серверу
|
|
|
44 |
|
45 |
-
#
|
46 |
playground_process = subprocess.Popen(
|
47 |
-
|
48 |
-
|
49 |
env=playground_env
|
50 |
)
|
51 |
processes.append(playground_process)
|
@@ -53,71 +61,173 @@ def main():
|
|
53 |
# Даем время Playground UI запуститься
|
54 |
time.sleep(5)
|
55 |
|
56 |
-
#
|
57 |
-
|
58 |
-
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
def do_GET(self):
|
61 |
-
self.
|
62 |
-
|
63 |
-
|
|
|
64 |
|
65 |
-
|
66 |
-
|
67 |
-
<html>
|
68 |
-
<head>
|
69 |
-
<title>TEN Agent - Hugging Face Space</title>
|
70 |
-
<meta charset="utf-8">
|
71 |
-
<style>
|
72 |
-
body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }
|
73 |
-
h1 { color: #333; }
|
74 |
-
.info { background: #f8f9fa; border-left: 4px solid #28a745; padding: 15px; margin-bottom: 20px; }
|
75 |
-
.warning { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-bottom: 20px; }
|
76 |
-
.endpoint { background: #e9ecef; padding: 10px; border-radius: 5px; font-family: monospace; }
|
77 |
-
.api { margin-top: 30px; }
|
78 |
-
</style>
|
79 |
-
</head>
|
80 |
-
<body>
|
81 |
-
<h1>TEN Agent запущен успешно!</h1>
|
82 |
-
<div class="info">
|
83 |
-
<p>TEN Agent API сервер работает на порту 8080.</p>
|
84 |
-
<p>Playground UI запущен на порту 3000.</p>
|
85 |
-
</div>
|
86 |
-
|
87 |
-
<div class="warning">
|
88 |
-
<p><strong>Внимание:</strong> Из-за ограничений Hugging Face Space, доступ к интерфейсу через прокси может быть нестабильным.</p>
|
89 |
-
</div>
|
90 |
-
|
91 |
-
<div class="api">
|
92 |
-
<h2>API эндпоинты:</h2>
|
93 |
-
<ul>
|
94 |
-
<li>API сервер: <span class="endpoint">http://localhost:8080</span></li>
|
95 |
-
<li>Playground UI: <span class="endpoint">http://localhost:3000</span></li>
|
96 |
-
</ul>
|
97 |
-
</div>
|
98 |
-
|
99 |
-
<h2>Инструкция по локальному использованию</h2>
|
100 |
-
<p>Для наилучшего опыта, подключите локальный Playground к этому API:</p>
|
101 |
-
<ol>
|
102 |
-
<li>Клонируйте репозиторий: <code>git clone https://github.com/TEN-framework/TEN-Agent.git</code></li>
|
103 |
-
<li>Перейдите в директорию playground: <code>cd TEN-Agent/playground</code></li>
|
104 |
-
<li>Установите зависимости: <code>pnpm install</code></li>
|
105 |
-
<li>Запустите Playground с подключением к API: <code>AGENT_SERVER_URL=https://nitrox-ten.hf.space pnpm dev</code></li>
|
106 |
-
<li>Откройте в браузере: <code>http://localhost:3000</code></li>
|
107 |
-
</ol>
|
108 |
-
|
109 |
-
<p>См. <a href="https://github.com/TEN-framework/TEN-Agent" target="_blank">документацию TEN Agent</a> для получения дополнительной информации.</p>
|
110 |
-
</body>
|
111 |
-
</html>
|
112 |
-
"""
|
113 |
|
114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
|
116 |
-
# Запускаем HTTP
|
117 |
-
port = 7860 # Hugging Face Space
|
118 |
-
print(f"Starting
|
119 |
-
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
|
122 |
except KeyboardInterrupt:
|
123 |
print("Shutting down...")
|
@@ -125,10 +235,16 @@ def main():
|
|
125 |
# Завершаем все процессы при выходе
|
126 |
for proc in processes:
|
127 |
try:
|
128 |
-
proc.
|
129 |
-
|
|
|
130 |
except:
|
131 |
-
proc.
|
|
|
|
|
|
|
|
|
|
|
132 |
|
133 |
return 0
|
134 |
|
|
|
6 |
from pathlib import Path
|
7 |
import signal
|
8 |
import shutil
|
9 |
+
import http.client
|
10 |
+
import socketserver
|
11 |
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
12 |
+
import threading
|
13 |
|
14 |
def main():
|
15 |
processes = []
|
|
|
27 |
print(f"ERROR: Playground directory not found at {playground_dir}", file=sys.stderr)
|
28 |
return 1
|
29 |
|
30 |
+
# Создаем директорию для логов с правами доступа
|
31 |
+
log_dir = "/tmp/ten_agent"
|
32 |
+
os.makedirs(log_dir, exist_ok=True)
|
33 |
+
# Даем всем права на запись в директорию логов
|
34 |
+
os.chmod(log_dir, 0o777)
|
35 |
|
36 |
# Запускаем API сервер
|
37 |
print("Starting TEN-Agent API server on port 8080...")
|
38 |
api_server_env = os.environ.copy()
|
39 |
+
api_server_env["LOG_PATH"] = log_dir
|
40 |
api_server_env["LOG_STDOUT"] = "true"
|
41 |
api_server_process = subprocess.Popen([str(api_binary)], env=api_server_env)
|
42 |
processes.append(api_server_process)
|
43 |
|
44 |
# Даем время API серверу запуститься
|
45 |
+
time.sleep(3)
|
46 |
|
47 |
+
# Запускаем Playground UI на порту 3000
|
48 |
print("Starting Playground UI on port 3000...")
|
49 |
playground_env = os.environ.copy()
|
50 |
playground_env["AGENT_SERVER_URL"] = "http://localhost:8080" # Подключаемся к локальному API серверу
|
51 |
+
playground_env["NODE_ENV"] = "production" # Убедимся, что запускаем в production режиме
|
52 |
|
53 |
+
# Запускаем Next.js с корректными параметрами
|
54 |
playground_process = subprocess.Popen(
|
55 |
+
"cd /app/playground && pnpm start -- --port 3000",
|
56 |
+
shell=True,
|
57 |
env=playground_env
|
58 |
)
|
59 |
processes.append(playground_process)
|
|
|
61 |
# Даем время Playground UI запуститься
|
62 |
time.sleep(5)
|
63 |
|
64 |
+
# Создаем эффективный прокси-сервер
|
65 |
+
class ProxyHandler(BaseHTTPRequestHandler):
|
66 |
+
# Отключаем логирование каждого запроса
|
67 |
+
def log_message(self, format, *args):
|
68 |
+
if args and args[0].startswith('GET /?logs=container'):
|
69 |
+
return # Игнорируем логи для запросов логов контейнера
|
70 |
+
sys.stderr.write("%s - - [%s] %s\n" %
|
71 |
+
(self.client_address[0],
|
72 |
+
self.log_date_time_string(),
|
73 |
+
format % args))
|
74 |
+
|
75 |
def do_GET(self):
|
76 |
+
self._handle_request('GET')
|
77 |
+
|
78 |
+
def do_POST(self):
|
79 |
+
self._handle_request('POST')
|
80 |
|
81 |
+
def do_PUT(self):
|
82 |
+
self._handle_request('PUT')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
|
84 |
+
def do_DELETE(self):
|
85 |
+
self._handle_request('DELETE')
|
86 |
+
|
87 |
+
def do_OPTIONS(self):
|
88 |
+
self._handle_request('OPTIONS')
|
89 |
+
|
90 |
+
def _handle_request(self, method):
|
91 |
+
try:
|
92 |
+
# Определяем, какой сервер должен обработать запрос
|
93 |
+
target_host = 'localhost'
|
94 |
+
|
95 |
+
# API endpoints идут на порт 8080 (API сервер)
|
96 |
+
if self.path.startswith('/health') or \
|
97 |
+
self.path.startswith('/list') or \
|
98 |
+
self.path.startswith('/graphs') or \
|
99 |
+
self.path.startswith('/start') or \
|
100 |
+
self.path.startswith('/stop') or \
|
101 |
+
self.path.startswith('/ping') or \
|
102 |
+
self.path.startswith('/token') or \
|
103 |
+
self.path.startswith('/dev-tmp') or \
|
104 |
+
self.path.startswith('/vector'):
|
105 |
+
target_port = 8080
|
106 |
+
else:
|
107 |
+
# Все остальные запросы (включая / и UI assets) идут на порт 3000 (Playground)
|
108 |
+
target_port = 3000
|
109 |
+
|
110 |
+
# Пробуем подключиться к целевому серверу
|
111 |
+
conn = http.client.HTTPConnection(target_host, target_port, timeout=30)
|
112 |
+
|
113 |
+
# Получаем данные запроса для POST/PUT
|
114 |
+
body = None
|
115 |
+
if method in ['POST', 'PUT']:
|
116 |
+
content_length = int(self.headers.get('Content-Length', 0))
|
117 |
+
body = self.rfile.read(content_length) if content_length > 0 else None
|
118 |
+
|
119 |
+
# Копируем все заголовки запроса
|
120 |
+
headers = {k: v for k, v in self.headers.items()}
|
121 |
+
|
122 |
+
# Исправляем Host заголовок
|
123 |
+
headers['Host'] = f"{target_host}:{target_port}"
|
124 |
+
|
125 |
+
# Отправляем запрос на правильный сервер
|
126 |
+
conn.request(method, self.path, body=body, headers=headers)
|
127 |
+
|
128 |
+
# Получаем ответ
|
129 |
+
response = conn.getresponse()
|
130 |
+
|
131 |
+
# Отправляем статус ответа
|
132 |
+
self.send_response(response.status)
|
133 |
+
|
134 |
+
# Копируем все заголовки ответа
|
135 |
+
for header, value in response.getheaders():
|
136 |
+
if header.lower() != 'transfer-encoding': # Исключаем заголо��ок transfer-encoding
|
137 |
+
self.send_header(header, value)
|
138 |
+
|
139 |
+
# Завершаем заголовки
|
140 |
+
self.end_headers()
|
141 |
+
|
142 |
+
# Отправляем тело ответа
|
143 |
+
chunk_size = 8192
|
144 |
+
while True:
|
145 |
+
chunk = response.read(chunk_size)
|
146 |
+
if not chunk:
|
147 |
+
break
|
148 |
+
self.wfile.write(chunk)
|
149 |
+
|
150 |
+
# Закрываем соединение
|
151 |
+
conn.close()
|
152 |
+
|
153 |
+
except Exception as e:
|
154 |
+
print(f"Proxy error for {method} {self.path}: {str(e)}", file=sys.stderr)
|
155 |
+
|
156 |
+
# Для запросов мониторинга не показываем ошибку
|
157 |
+
if self.path == '/?logs=container':
|
158 |
+
self.send_response(200)
|
159 |
+
self.send_header('Content-type', 'text/plain')
|
160 |
+
self.end_headers()
|
161 |
+
self.wfile.write(b"OK")
|
162 |
+
return
|
163 |
+
|
164 |
+
# Отправляем страницу с ошибкой и разъяснением
|
165 |
+
self.send_response(500)
|
166 |
+
self.send_header('Content-type', 'text/html; charset=utf-8')
|
167 |
+
self.end_headers()
|
168 |
+
|
169 |
+
error_message = f"""
|
170 |
+
<!DOCTYPE html>
|
171 |
+
<html>
|
172 |
+
<head>
|
173 |
+
<title>TEN Agent - Error</title>
|
174 |
+
<meta charset="utf-8">
|
175 |
+
<style>
|
176 |
+
body {{ font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }}
|
177 |
+
h1 {{ color: #dc3545; }}
|
178 |
+
.error {{ background: #f8d7da; border-left: 4px solid #dc3545; padding: 15px; margin-bottom: 20px; }}
|
179 |
+
pre {{ background: #f8f9fa; padding: 10px; border-radius: 5px; overflow-x: auto; }}
|
180 |
+
</style>
|
181 |
+
</head>
|
182 |
+
<body>
|
183 |
+
<h1>Произошла ошибка при обработке запроса</h1>
|
184 |
+
<div class="error">
|
185 |
+
<p><strong>Детали ошибки:</strong> {str(e)}</p>
|
186 |
+
<p>Целевой порт: {target_port}</p>
|
187 |
+
</div>
|
188 |
+
<p>Система пытается запустить все компоненты. Попробуйте обновить страницу через минуту.</p>
|
189 |
+
</body>
|
190 |
+
</html>
|
191 |
+
"""
|
192 |
+
|
193 |
+
self.wfile.write(error_message.encode('utf-8'))
|
194 |
|
195 |
+
# Запускаем HTTP прокси-сервер
|
196 |
+
port = 7860 # Hugging Face Space ожидает сервер на порту 7860
|
197 |
+
print(f"Starting proxy server on port {port}...")
|
198 |
+
|
199 |
+
# Разрешаем повторное использование адреса и порта
|
200 |
+
class ReuseAddressServer(socketserver.ThreadingTCPServer):
|
201 |
+
allow_reuse_address = True
|
202 |
+
daemon_threads = True
|
203 |
+
|
204 |
+
server = ReuseAddressServer(('0.0.0.0', port), ProxyHandler)
|
205 |
+
|
206 |
+
# Запускаем сервер
|
207 |
+
server_thread = threading.Thread(target=server.serve_forever)
|
208 |
+
server_thread.daemon = True
|
209 |
+
server_thread.start()
|
210 |
+
|
211 |
+
# Продолжаем выполнение, чтобы можно было обработать сигналы остановки
|
212 |
+
while True:
|
213 |
+
# Проверяем, что все процессы еще живы
|
214 |
+
if not api_server_process.poll() is None:
|
215 |
+
print("API server has stopped, restarting...")
|
216 |
+
api_server_process = subprocess.Popen([str(api_binary)], env=api_server_env)
|
217 |
+
processes = [p for p in processes if p != api_server_process]
|
218 |
+
processes.append(api_server_process)
|
219 |
+
|
220 |
+
if not playground_process.poll() is None:
|
221 |
+
print("Playground UI has stopped, restarting...")
|
222 |
+
playground_process = subprocess.Popen(
|
223 |
+
"cd /app/playground && pnpm start -- --port 3000",
|
224 |
+
shell=True,
|
225 |
+
env=playground_env
|
226 |
+
)
|
227 |
+
processes = [p for p in processes if p != playground_process]
|
228 |
+
processes.append(playground_process)
|
229 |
+
|
230 |
+
time.sleep(10)
|
231 |
|
232 |
except KeyboardInterrupt:
|
233 |
print("Shutting down...")
|
|
|
235 |
# Завершаем все процессы при выходе
|
236 |
for proc in processes:
|
237 |
try:
|
238 |
+
if proc and proc.poll() is None:
|
239 |
+
proc.terminate()
|
240 |
+
proc.wait(timeout=5)
|
241 |
except:
|
242 |
+
if proc and proc.poll() is None:
|
243 |
+
proc.kill()
|
244 |
+
|
245 |
+
# Останавливаем сервер
|
246 |
+
if 'server' in locals():
|
247 |
+
server.shutdown()
|
248 |
|
249 |
return 0
|
250 |
|