#!/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())