|
|
|
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) |
|
|
|
|
|
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_path = Path("/app/.env") |
|
tmp_env_path = Path("/tmp/ten_agent/.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_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_dir = AGENTS_DIR / "examples" |
|
examples_dir.mkdir(exist_ok=True) |
|
|
|
|
|
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_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_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_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_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}") |
|
|
|
|
|
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_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: |
|
|
|
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}") |
|
|
|
|
|
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") |
|
|
|
|
|
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): |
|
|
|
if self.path.startswith('/api/'): |
|
api_path = self.path[5:] |
|
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) |
|
|
|
|
|
for header, value in response.getheaders(): |
|
if header.lower() not in ('transfer-encoding', 'connection'): |
|
self.send_header(header, value) |
|
|
|
|
|
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_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) |
|
|
|
|
|
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 |
|
|
|
|
|
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): |
|
|
|
if self.path.startswith('/api/'): |
|
api_path = self.path[5:] |
|
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) |
|
|
|
|
|
for header, value in response.getheaders(): |
|
if header.lower() not in ('transfer-encoding', 'connection'): |
|
self.send_header(header, value) |
|
|
|
|
|
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_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) |
|
|
|
|
|
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() |
|
|
|
|
|
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/") |
|
|
|
|
|
def check_api_availability(): |
|
while True: |
|
try: |
|
|
|
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) |
|
|
|
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") |
|
|
|
|
|
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_home = "/tmp/pnpm-store" |
|
os.makedirs(pnpm_home, exist_ok=True) |
|
env["PNPM_HOME"] = pnpm_home |
|
env["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_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}") |
|
|
|
|
|
os.makedirs("/tmp/.npm-cache", exist_ok=True) |
|
os.makedirs("/tmp/.npm-tmp", exist_ok=True) |
|
|
|
|
|
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_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...") |
|
|
|
|
|
logger.info("Trying to start Playground with node directly...") |
|
|
|
|
|
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_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_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() |
|
|
|
|
|
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: |
|
|
|
if api_process.poll() is not None: |
|
logger.error(f"API server exited with code {api_process.returncode}") |
|
break |
|
|
|
|
|
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()) |