ten / fallback.py
3v324v23's picture
Добавлен прокси для статического Playground UI, работающий с HuggingFace Space
bf2f692
#!/usr/bin/env python3
import os
import sys
import json
import time
import shutil
import signal
import logging
import subprocess
from pathlib import Path
import tempfile
import threading
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]
)
logger = logging.getLogger("TEN-Agent")
# Пути к директориям
TMP_DIR = Path("/tmp/ten_user")
LOG_DIR = TMP_DIR / "logs"
AGENTS_DIR = TMP_DIR / "agents"
RAG_DIR = TMP_DIR / "rag_data"
def setup_environment():
"""Настройка базового окружения"""
logger.info("Current directory: %s", os.getcwd())
logger.info("Current user: %s", os.environ.get('USER', 'unknown'))
# Информация об окружении
logger.info("HOME: %s", os.environ.get('HOME', 'Not set'))
logger.info("PATH: %s", os.environ.get('PATH', 'Not set'))
# Проверка прав доступа во временной директории
logger.info("Checking permissions for %s", "/tmp")
try:
result = subprocess.run(["ls", "-la", "/tmp"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info(" - %s", result.stdout.decode('utf-8').strip())
# Проверка возможности записи
test_file = "/tmp/ten_test_write.txt"
can_write = False
try:
with open(test_file, 'w') as f:
f.write("Test write")
can_write = True
logger.info(" - Can write: Yes")
os.remove(test_file)
except Exception as e:
logger.info(" - Can write: No (%s)", str(e))
if not can_write:
logger.error("Cannot write to /tmp. This is required for operation.")
sys.exit(1)
except Exception as e:
logger.error("Error checking permissions: %s", str(e))
def create_directory_structure():
"""Создание необходимой структуры директорий"""
logger.info("Creating user directory...")
# Создание основной директории
TMP_DIR.mkdir(exist_ok=True)
logger.info(f"Created directory structure at {TMP_DIR}")
# Создание поддиректорий
AGENTS_DIR.mkdir(exist_ok=True)
RAG_DIR.mkdir(exist_ok=True)
LOG_DIR.mkdir(exist_ok=True)
# Создаем папки, которые ожидает API сервер
server_log_dir = Path("/tmp/ten_agent/logs")
server_log_dir.parent.mkdir(exist_ok=True)
server_log_dir.mkdir(exist_ok=True)
logger.info(f"Created log directory at {LOG_DIR}")
logger.info(f"Created server log directory at {server_log_dir}")
def create_env_file():
"""Создание файла .env для API сервера"""
logger.info("Creating .env file...")
# Создаем .env файл в директории сервера
env_path = Path("/app/.env")
tmp_env_path = Path("/tmp/ten_agent/.env")
# Базовое содержимое .env
env_content = """
LOG_LEVEL=debug
LOG_DIR=/tmp/ten_agent/logs
AGENT_SERVER_DIRECTORY=/tmp/ten_user/agents
AGENT_SERVER_HOST=0.0.0.0
AGENT_SERVER_PORT=8080
PUBLIC_URL=http://localhost:7860
DISABLE_CAMERA=true
"""
# Записываем файл во временную директорию
with open(tmp_env_path, 'w') as f:
f.write(env_content)
# Пытаемся скопировать в основную директорию (может не сработать из-за прав доступа)
try:
with open(env_path, 'w') as f:
f.write(env_content)
logger.info(f"Created .env file at {env_path}")
except Exception as e:
logger.warning(f"Could not create .env in /app, using temporary one: {e}")
logger.info(f"Created .env file at {tmp_env_path}")
# Также устанавливаем переменные окружения напрямую
os.environ["LOG_LEVEL"] = "debug"
os.environ["LOG_DIR"] = "/tmp/ten_agent/logs"
os.environ["AGENT_SERVER_DIRECTORY"] = str(AGENTS_DIR)
os.environ["AGENT_SERVER_HOST"] = "0.0.0.0"
os.environ["AGENT_SERVER_PORT"] = "8080"
os.environ["PUBLIC_URL"] = "http://localhost:7860"
os.environ["DISABLE_CAMERA"] = "true"
def create_basic_config():
"""Создание базовых конфигурационных файлов"""
logger.info("Creating basic configuration files...")
# Создание property.json
property_path = AGENTS_DIR / "property.json"
if not property_path.exists():
property_data = {
"_ten": {
"version": "0.0.1"
},
"name": "TEN Agent Example",
"version": "0.0.1",
"extensions": ["openai_chatgpt"],
"description": "A basic voice agent with OpenAI",
"graphs": [
{
"id": "voice_agent",
"name": "Voice Agent",
"description": "Basic voice agent with OpenAI",
"file": "examples/voice_agent.json"
},
{
"id": "chat_agent",
"name": "Chat Agent",
"description": "Simple chat agent",
"file": "examples/chat_agent.json"
}
]
}
with open(property_path, 'w') as f:
json.dump(property_data, f, indent=2)
# Создание директории examples
examples_dir = AGENTS_DIR / "examples"
examples_dir.mkdir(exist_ok=True)
# Создание voice_agent.json
voice_agent_path = examples_dir / "voice_agent.json"
if not voice_agent_path.exists():
voice_data = {
"_ten": {
"version": "0.0.1"
},
"nodes": [
{
"id": "start",
"type": "start",
"data": {
"x": 100,
"y": 100
}
},
{
"id": "openai_chatgpt",
"type": "openai_chatgpt",
"data": {
"x": 300,
"y": 200,
"properties": {
"model": "gpt-3.5-turbo",
"temperature": 0.7,
"system_prompt": "Вы полезный голосовой помощник."
}
}
},
{
"id": "end",
"type": "end",
"data": {
"x": 500,
"y": 100
}
}
],
"edges": [
{
"id": "start_to_chatgpt",
"source": "start",
"target": "openai_chatgpt"
},
{
"id": "chatgpt_to_end",
"source": "openai_chatgpt",
"target": "end"
}
],
"groups": [],
"templates": [],
"root": "start"
}
with open(voice_agent_path, 'w') as f:
json.dump(voice_data, f, indent=2)
# Создание chat_agent.json
chat_agent_path = examples_dir / "chat_agent.json"
if not chat_agent_path.exists():
chat_data = {
"_ten": {
"version": "0.0.1"
},
"nodes": [
{
"id": "start",
"type": "start",
"data": {
"x": 100,
"y": 100
}
},
{
"id": "openai_chatgpt",
"type": "openai_chatgpt",
"data": {
"x": 300,
"y": 200,
"properties": {
"model": "gpt-3.5-turbo",
"temperature": 0.7,
"system_prompt": "Вы полезный чат-бот."
}
}
},
{
"id": "end",
"type": "end",
"data": {
"x": 500,
"y": 100
}
}
],
"edges": [
{
"id": "start_to_chatgpt",
"source": "start",
"target": "openai_chatgpt"
},
{
"id": "chatgpt_to_end",
"source": "openai_chatgpt",
"target": "end"
}
],
"groups": [],
"templates": [],
"root": "start"
}
with open(chat_agent_path, 'w') as f:
json.dump(chat_data, f, indent=2)
# Создание manifest.json
manifest_path = AGENTS_DIR / "manifest.json"
if not manifest_path.exists():
manifest_data = {
"_ten": {
"version": "0.0.1"
},
"name": "default",
"agents": [
{
"name": "voice_agent",
"description": "A simple voice agent",
"type": "voice"
},
{
"name": "chat_agent",
"description": "A text chat agent",
"type": "chat"
}
]
}
with open(manifest_path, 'w') as f:
json.dump(manifest_data, f, indent=2)
logger.info("Basic configuration files created successfully.")
def run_api_server():
"""Запуск API сервера"""
logger.info(f"Starting API server with agents directory: {AGENTS_DIR}")
# Запуск API сервера напрямую из директории server
api_binary = Path("/app/server/bin/api")
if not api_binary.exists():
logger.error(f"API binary not found at {api_binary}")
raise FileNotFoundError(f"API binary not found at {api_binary}")
# Настраиваем рабочую директорию и окружение
work_dir = Path("/app/server")
env = os.environ.copy()
env["AGENT_SERVER_DIRECTORY"] = str(AGENTS_DIR)
env["LOG_DIR"] = "/tmp/ten_agent/logs"
cmd = [str(api_binary)]
logger.info(f"Running command: {' '.join(cmd)}")
logger.info(f"With AGENT_SERVER_DIRECTORY={env['AGENT_SERVER_DIRECTORY']}")
logger.info(f"With LOG_DIR={env['LOG_DIR']}")
# Запуск процесса сервера
api_process = subprocess.Popen(
cmd,
cwd=str(work_dir),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
# Логирование вывода процесса
def log_output(stream, prefix):
for line in stream:
logger.info(f"{prefix}: {line.strip()}")
threading.Thread(target=log_output, args=(api_process.stdout, "API"), daemon=True).start()
threading.Thread(target=log_output, args=(api_process.stderr, "API ERROR"), daemon=True).start()
# Проверка, что процесс запустился
time.sleep(2)
if api_process.poll() is not None:
logger.error(f"API server failed to start with exit code {api_process.returncode}")
# Если процесс упал, логируем его вывод
stdout, stderr = api_process.communicate()
logger.error(f"API stdout: {stdout}")
logger.error(f"API stderr: {stderr}")
# Пробуем запустить с другими аргументами, если первая попытка не удалась
logger.info("Trying alternative API server launch method...")
try:
# Пробуем запустить с помощью wrapper скрипта
wrapper_script = Path("/app/api_wrapper.py")
if wrapper_script.exists():
logger.info("Using api_wrapper.py as fallback")
env["AGENT_DIR"] = str(AGENTS_DIR)
wrapper_process = subprocess.Popen(
["python3", str(wrapper_script)],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
threading.Thread(target=log_output, args=(wrapper_process.stdout, "WRAPPER"), daemon=True).start()
threading.Thread(target=log_output, args=(wrapper_process.stderr, "WRAPPER ERROR"), daemon=True).start()
time.sleep(2)
if wrapper_process.poll() is not None:
logger.error(f"Wrapper script failed with code {wrapper_process.returncode}")
stdout, stderr = wrapper_process.communicate()
logger.error(f"Wrapper stdout: {stdout}")
logger.error(f"Wrapper stderr: {stderr}")
raise RuntimeError("All API server launch methods failed")
logger.info("API server started through wrapper script")
return wrapper_process
except Exception as e:
logger.error(f"Alternative launch method also failed: {e}")
raise RuntimeError(f"API server failed to start with exit code {api_process.returncode}")
logger.info("API server started successfully")
return api_process
def run_proxy_server():
"""Запуск прокси-сервера для обработки запросов TEN-Agent Designer API"""
# Код для запуска прокси-сервера
logger.info("Starting proxy server for Designer API requests")
import http.server
import socketserver
import urllib.request
import urllib.error
import json
class ProxyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def do_POST(self):
logger.info(f"Received POST request: {self.path}")
# Перенаправляем запрос /api/dev/v1/packages/reload на /graphs
if self.path.startswith('/api/dev/v1/packages/reload') or self.path.startswith('/api/designer/v1/packages/reload'):
try:
logger.info("Redirecting to /graphs")
try:
with urllib.request.urlopen("http://localhost:8080/graphs") as response:
data = response.read().decode('utf-8')
# Если сервер вернул пустой ответ или ошибку, создаем свой собственный ответ
if not data or "Invalid format" in data:
logger.info("Server returned error or empty response, creating custom response")
# Создаем хардкодный список графов из property.json
property_path = AGENTS_DIR / "property.json"
if property_path.exists():
try:
with open(property_path, 'r') as f:
property_data = json.load(f)
graphs = property_data.get("graphs", [])
except Exception as e:
logger.error(f"Error reading property.json: {e}")
graphs = []
else:
graphs = []
# Добавляем обязательные поля для каждого графа
for graph in graphs:
if "id" not in graph:
graph["id"] = graph.get("name", "").lower().replace(" ", "_")
else:
try:
# Пытаемся разобрать JSON из ответа
graphs = json.loads(data)
except json.JSONDecodeError:
logger.error(f"Error parsing JSON from server response: {data}")
graphs = []
# Форматируем ответ в нужном формате для фронтенда
formatted_response = {
"data": graphs,
"status": 200,
"message": "Success"
}
response_data = json.dumps(formatted_response).encode('utf-8')
# Отправляем ответ
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(response_data))
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(response_data)
logger.info(f"Sent response: {response_data.decode('utf-8')}")
except urllib.error.URLError as e:
logger.error(f"Error when redirecting: {e}")
# В случае ошибки, отправляем хардкодный ответ
property_path = AGENTS_DIR / "property.json"
if property_path.exists():
try:
with open(property_path, 'r') as f:
property_data = json.load(f)
graphs = property_data.get("graphs", [])
except Exception as e:
logger.error(f"Error reading property.json: {e}")
graphs = []
else:
graphs = []
# Добавляем обязательные поля для каждого графа
for graph in graphs:
if "id" not in graph:
graph["id"] = graph.get("name", "").lower().replace(" ", "_")
formatted_response = {
"data": graphs,
"status": 200,
"message": "Success"
}
response_data = json.dumps(formatted_response).encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(response_data))
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(response_data)
logger.info(f"Sent hardcoded response: {response_data.decode('utf-8')}")
except Exception as e:
logger.error(f"Unexpected error: {e}")
self.send_error(500, f"Internal Server Error: {str(e)}")
else:
self.send_error(404, "Not Found")
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
def do_GET(self):
logger.info(f"Received GET request: {self.path}")
# Перенаправляем запрос /api/designer/v1/addons/extensions
if self.path.startswith('/api/designer/v1/addons/extensions'):
# Отправляем хардкодный ответ со списком расширений
extensions = [
{
"id": "openai_chatgpt",
"name": "OpenAI ChatGPT",
"version": "0.0.1",
"description": "Integration with OpenAI ChatGPT API",
"nodes": [
{
"id": "openai_chatgpt",
"name": "OpenAI ChatGPT",
"category": "AI",
"description": "Sends message to OpenAI ChatGPT API"
}
]
}
]
formatted_response = {
"data": extensions,
"status": 200,
"message": "Success"
}
response_data = json.dumps(formatted_response).encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(response_data))
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(response_data)
logger.info(f"Sent response for extensions: {response_data.decode('utf-8')}")
else:
self.send_error(404, "Not Found")
# Запуск HTTP сервера
port = 49483
httpd = socketserver.ThreadingTCPServer(("", port), ProxyHTTPRequestHandler)
logger.info(f"Proxy server started at port {port}")
# Запуск в отдельном потоке
proxy_thread = threading.Thread(target=httpd.serve_forever)
proxy_thread.daemon = True
proxy_thread.start()
return httpd
def run_playground_proxy():
"""Запускает прокси-сервер для обслуживания Playground UI и проксирования запросов к API"""
import http.server
import socketserver
from urllib.parse import urljoin, urlparse
import urllib.request
import urllib.error
class PlaygroundProxyHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
logger.info(f"PLAYGROUND-PROXY: {format % args}")
def do_GET(self):
# Проксирование запросов к API
if self.path.startswith('/api/'):
api_path = self.path[5:] # Удаляем '/api/' из пути
api_url = f"http://localhost:8080/{api_path}"
try:
logger.info(f"Proxying GET request to API: {api_url}")
with urllib.request.urlopen(api_url) as response:
data = response.read()
self.send_response(response.status)
# Копируем все заголовки из ответа API
for header, value in response.getheaders():
if header.lower() not in ('transfer-encoding', 'connection'):
self.send_header(header, value)
# Устанавливаем CORS заголовки
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(data)
except urllib.error.URLError as e:
logger.error(f"Error proxying GET request to API: {e}")
self.send_error(502, f"Error proxying to API: {e}")
return
# Проксирование запросов к серверу графического редактора
elif self.path.startswith('/designer/'):
designer_path = self.path[10:] # Удаляем '/designer/' из пути
designer_url = f"http://localhost:49483/{designer_path}"
try:
logger.info(f"Proxying GET request to designer: {designer_url}")
with urllib.request.urlopen(designer_url) as response:
data = response.read()
self.send_response(response.status)
# Копируем все заголовки из ответа дизайнера
for header, value in response.getheaders():
if header.lower() not in ('transfer-encoding', 'connection'):
self.send_header(header, value)
# Устанавливаем CORS заголовки
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(data)
except urllib.error.URLError as e:
logger.error(f"Error proxying GET request to designer: {e}")
self.send_error(502, f"Error proxying to designer: {e}")
return
# Перенаправление корневого пути на Playground UI
elif self.path == '/' or self.path == '':
self.send_response(302)
self.send_header('Location', 'https://ten-framework.github.io/TEN-Playground-Static/')
self.end_headers()
return
# Для всех остальных запросов пытаемся обслужить статический файл
else:
self.send_error(404, "File Not Found")
def do_POST(self):
# Проксирование POST запросов к API
if self.path.startswith('/api/'):
api_path = self.path[5:] # Удаляем '/api/' из пути
api_url = f"http://localhost:8080/{api_path}"
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length) if content_length > 0 else b''
try:
logger.info(f"Proxying POST request to API: {api_url}")
request = urllib.request.Request(
api_url,
data=post_data,
headers={k: v for k, v in self.headers.items() if k.lower() not in ('host', 'content-length')},
method='POST'
)
with urllib.request.urlopen(request) as response:
data = response.read()
self.send_response(response.status)
# Копируем все заголовки из ответа API
for header, value in response.getheaders():
if header.lower() not in ('transfer-encoding', 'connection'):
self.send_header(header, value)
# Устанавливаем CORS заголовки
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(data)
except urllib.error.URLError as e:
logger.error(f"Error proxying POST request to API: {e}")
self.send_error(502, f"Error proxying to API: {e}")
# Проксирование запросов к серверу графического редактора
elif self.path.startswith('/designer/'):
designer_path = self.path[10:] # Удаляем '/designer/' из пути
designer_url = f"http://localhost:49483/{designer_path}"
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length) if content_length > 0 else b''
try:
logger.info(f"Proxying POST request to designer: {designer_url}")
request = urllib.request.Request(
designer_url,
data=post_data,
headers={k: v for k, v in self.headers.items() if k.lower() not in ('host', 'content-length')},
method='POST'
)
with urllib.request.urlopen(request) as response:
data = response.read()
self.send_response(response.status)
# Копируем все заголовки из ответа дизайнера
for header, value in response.getheaders():
if header.lower() not in ('transfer-encoding', 'connection'):
self.send_header(header, value)
# Устанавливаем CORS заголовки
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(data)
except urllib.error.URLError as e:
logger.error(f"Error proxying POST request to designer: {e}")
self.send_error(502, f"Error proxying to designer: {e}")
else:
self.send_error(404, "Not Found")
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
self.end_headers()
# Запуск прокси-сервера на порту 7860 (стандартный порт HF Space)
port = 7860
httpd = socketserver.ThreadingTCPServer(("", port), PlaygroundProxyHandler)
logger.info(f"Playground proxy server started on port {port}")
logger.info(f"Access Playground at: https://nitrox-ten.hf.space/")
# В отдельном потоке запускаем проверку доступности API сервера
def check_api_availability():
while True:
try:
# Проверка /graphs API
with urllib.request.urlopen("http://localhost:8080/graphs") as response:
if response.status == 200:
data = response.read().decode('utf-8')
logger.info(f"API /graphs endpoint is available: {data}")
except Exception as e:
logger.warning(f"API check failed: {e}")
time.sleep(30) # Проверяем каждые 30 секунд
api_check_thread = threading.Thread(target=check_api_availability, daemon=True)
api_check_thread.start()
# Запускаем сервер в основном потоке
httpd.serve_forever()
def run_playground():
"""Запуск Playground UI"""
logger.info("Starting Playground UI in development mode")
# Настройка окружения для Playground
env = os.environ.copy()
env["PORT"] = "7860"
env["AGENT_SERVER_URL"] = "http://localhost:8080"
env["NEXT_PUBLIC_EDIT_GRAPH_MODE"] = "true"
env["NEXT_PUBLIC_DISABLE_CAMERA"] = "true"
# Настройка PNPM для использования временной директории для кэша
pnpm_home = "/tmp/pnpm-store"
os.makedirs(pnpm_home, exist_ok=True)
env["PNPM_HOME"] = pnpm_home
env["HOME"] = "/tmp" # Установка HOME в /tmp для всех инструментов
env["XDG_CONFIG_HOME"] = "/tmp/.config"
env["XDG_CACHE_HOME"] = "/tmp/.cache"
env["XDG_DATA_HOME"] = "/tmp/.local/share"
# Создаем необходимые директории
os.makedirs("/tmp/.config", exist_ok=True)
os.makedirs("/tmp/.cache", exist_ok=True)
os.makedirs("/tmp/.local/share", exist_ok=True)
logger.info(f"Set PNPM_HOME to {pnpm_home}")
logger.info(f"Set HOME to {env['HOME']}")
playground_dir = Path("/app/playground")
if not playground_dir.exists():
logger.error(f"Playground directory not found at {playground_dir}")
raise FileNotFoundError(f"Playground directory not found at {playground_dir}")
# Создаем .npmrc файл в директории playground для настройки npm
npmrc_path = playground_dir / ".npmrc"
try:
with open(npmrc_path, 'w') as f:
f.write(f"cache=/tmp/.npm-cache\n")
f.write(f"tmp=/tmp/.npm-tmp\n")
logger.info(f"Created .npmrc file at {npmrc_path}")
except Exception as e:
logger.warning(f"Could not create .npmrc: {e}")
# Создаем директории для npm
os.makedirs("/tmp/.npm-cache", exist_ok=True)
os.makedirs("/tmp/.npm-tmp", exist_ok=True)
# Пробуем сначала запустить с помощью npx next dev
logger.info("Trying to start Playground with npx next dev...")
cmd = ["npx", "next", "dev", "--port", "7860"]
logger.info(f"Running command in {playground_dir}: {' '.join(cmd)}")
# Запуск процесса Playground
playground_process = subprocess.Popen(
cmd,
cwd=str(playground_dir),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
# Логирование вывода процесса
def log_output(stream, prefix):
for line in stream:
logger.info(f"{prefix}: {line.strip()}")
threading.Thread(target=log_output, args=(playground_process.stdout, "PLAYGROUND"), daemon=True).start()
threading.Thread(target=log_output, args=(playground_process.stderr, "PLAYGROUND ERROR"), daemon=True).start()
# Проверка, что процесс запустился
time.sleep(5)
if playground_process.poll() is not None:
logger.warning(f"npx next dev failed with code {playground_process.returncode}, trying alternative method...")
# Если первый метод не работает, пробуем запустить напрямую node
logger.info("Trying to start Playground with node directly...")
# Ищем файл .next/server/app/page.js
next_page_js = playground_dir / ".next/server/app/page.js"
if next_page_js.exists():
cmd = ["node", str(next_page_js)]
logger.info(f"Running command: {' '.join(cmd)}")
playground_process = subprocess.Popen(
cmd,
cwd=str(playground_dir),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
threading.Thread(target=log_output, args=(playground_process.stdout, "PLAYGROUND (NODE)"), daemon=True).start()
threading.Thread(target=log_output, args=(playground_process.stderr, "PLAYGROUND ERROR (NODE)"), daemon=True).start()
time.sleep(3)
if playground_process.poll() is not None:
logger.error(f"All Playground UI launch methods failed")
logger.info("Starting playground proxy as fallback")
# Запускаем прокси-сервер для Playground
playground_thread = threading.Thread(target=run_playground_proxy, daemon=True)
playground_thread.start()
# Создаем заглушку для процесса
playground_process = subprocess.Popen(
["tail", "-f", "/dev/null"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
else:
logger.info("Starting playground proxy as fallback")
# Запускаем прокси-сервер для Playground
playground_thread = threading.Thread(target=run_playground_proxy, daemon=True)
playground_thread.start()
# Создаем заглушку для процесса
playground_process = subprocess.Popen(
["tail", "-f", "/dev/null"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
logger.info("Playground UI started successfully or proxy is running")
return playground_process
def main():
"""Основная функция запуска"""
try:
# Настройка окружения
setup_environment()
# Создание структуры директорий
create_directory_structure()
# Создаем .env файл
create_env_file()
# Создание базовых конфигурационных файлов
create_basic_config()
# Запуск сервисов
api_process = run_api_server()
proxy_server = run_proxy_server()
playground_process = run_playground()
# Отслеживание статуса процессов
logger.info("All services started. Monitoring status...")
try:
while True:
# Проверка статуса API сервера
if api_process.poll() is not None:
logger.error(f"API server exited with code {api_process.returncode}")
break
# Проверка статуса Playground
if playground_process.poll() is not None:
logger.error(f"Playground UI exited with code {playground_process.returncode}")
break
time.sleep(5)
except KeyboardInterrupt:
logger.info("Received interrupt signal")
finally:
# Остановка сервисов
logger.info("Stopping services...")
try:
if api_process and api_process.poll() is None:
api_process.terminate()
api_process.wait(timeout=5)
except Exception as e:
logger.warning(f"Error stopping API server: {e}")
try:
if playground_process and playground_process.poll() is None:
playground_process.terminate()
playground_process.wait(timeout=5)
except Exception as e:
logger.warning(f"Error stopping Playground UI: {e}")
try:
proxy_server.shutdown()
except Exception as e:
logger.warning(f"Error stopping proxy server: {e}")
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
return 1
return 0
if __name__ == "__main__":
# Настройка обработки сигналов
signal.signal(signal.SIGINT, lambda sig, frame: sys.exit(0))
signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit(0))
# Запуск основной функции
sys.exit(main())