#!/usr/bin/env python3 import os import subprocess import sys import time import json from pathlib import Path import signal import threading import shutil import http.server import socketserver import urllib.request import urllib.error import gradio as gr # Проверяем, запущены ли мы в HuggingFace Space IS_HF_SPACE = os.environ.get("SPACE_ID") is not None print(f"Running in HuggingFace Space: {IS_HF_SPACE}") # Конфигурация путей TMP_DIR = Path("/tmp/ten_user") AGENTS_DIR = TMP_DIR / "agents" LOGS_DIR = TMP_DIR / "logs" NEXTJS_PORT = int(os.environ.get("UI_PORT", 3000)) API_PORT = int(os.environ.get("API_PORT", 8080)) GRADIO_PORT = int(os.environ.get("GRADIO_PORT", 7860)) # Адрес для подключения к UI в iframe if IS_HF_SPACE: # В HuggingFace Space нужно использовать внутренний URL UI_URL = f"https://{os.environ.get('SPACE_ID', 'unknown-space')}.hf.space/_next/iframe/3000" INTERNAL_HOST = "0.0.0.0" else: # В локальном окружении используем localhost UI_URL = f"http://localhost:{NEXTJS_PORT}" INTERNAL_HOST = "localhost" def create_directories(): """Создает необходимые директории""" print("Создание директорий...") # Создаем только директории, без попытки создания файлов TMP_DIR.mkdir(exist_ok=True, parents=True) AGENTS_DIR.mkdir(exist_ok=True, parents=True) LOGS_DIR.mkdir(exist_ok=True, parents=True) # Не создаем файл server.log, т.к. он вызывает ошибку прав доступа # (LOGS_DIR / "server.log").touch() print(f"Директории созданы в {TMP_DIR}") # Выводим информацию о правах доступа для отладки print(f"Права доступа для {TMP_DIR}: {os.stat(TMP_DIR).st_mode}") if LOGS_DIR.exists(): print(f"Директория логов создана: {LOGS_DIR}") print(f"Права доступа для {LOGS_DIR}: {os.stat(LOGS_DIR).st_mode}") else: print(f"Директория логов не создана: {LOGS_DIR}") # Пробуем записать лог в /tmp напрямую, где должны быть права на запись try: tmp_log_file = Path("/tmp/ten_server.log") tmp_log_file.touch() print(f"Создан файл лога в /tmp: {tmp_log_file}") except Exception as e: print(f"Не удалось создать файл лога в /tmp: {e}") # Это не критическая ошибка, продолжаем работу def create_config_files(): """Создает базовые файлы конфигурации""" print("Создание конфигурационных файлов...") # Используем директорию в /tmp напрямую, без вложенных директорий config_dir = Path("/tmp") property_file_path = config_dir / "property.json" voice_agent_path = config_dir / "voice_agent.json" chat_agent_path = config_dir / "chat_agent.json" # Создаем property.json с графами property_data = { "name": "TEN Agent Demo", "version": "0.0.1", "extensions": ["openai_chatgpt", "elevenlabs_tts", "deepgram_asr"], "description": "TEN Agent on Hugging Face Space", "graphs": [ { "name": "Voice Agent", "description": "Basic voice agent with OpenAI and ElevenLabs", "file": "voice_agent.json" }, { "name": "Chat Agent", "description": "Simple chat agent with OpenAI", "file": "chat_agent.json" } ] } try: with open(property_file_path, "w") as f: json.dump(property_data, f, indent=2) print(f"Файл {property_file_path} создан успешно") except Exception as e: print(f"Ошибка при создании {property_file_path}: {e}") return False # Создаем voice_agent.json voice_agent = { "_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": "You are a helpful assistant." } } }, { "id": "elevenlabs_tts", "type": "elevenlabs_tts", "data": { "x": 500, "y": 200, "properties": { "voice_id": "21m00Tcm4TlvDq8ikWAM" } } }, { "id": "deepgram_asr", "type": "deepgram_asr", "data": { "x": 300, "y": 300, "properties": { "language": "ru" } } }, { "id": "end", "type": "end", "data": {"x": 700, "y": 100} } ], "edges": [ {"id": "start_to_chatgpt", "source": "start", "target": "openai_chatgpt"}, {"id": "chatgpt_to_tts", "source": "openai_chatgpt", "target": "elevenlabs_tts"}, {"id": "tts_to_end", "source": "elevenlabs_tts", "target": "end"}, {"id": "asr_to_chatgpt", "source": "deepgram_asr", "target": "openai_chatgpt"} ], "groups": [], "templates": [], "root": "start" } try: with open(voice_agent_path, "w") as f: json.dump(voice_agent, f, indent=2) print(f"Файл {voice_agent_path} создан успешно") except Exception as e: print(f"Ошибка при создании {voice_agent_path}: {e}") return False # Создаем chat_agent.json (упрощенная версия) chat_agent = { "_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": "You are a helpful chat assistant." } } }, { "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" } try: with open(chat_agent_path, "w") as f: json.dump(chat_agent, f, indent=2) print(f"Файл {chat_agent_path} создан успешно") except Exception as e: print(f"Ошибка при создании {chat_agent_path}: {e}") return False # Обновляем глобальную переменную AGENTS_DIR для использования нового пути global AGENTS_DIR AGENTS_DIR = config_dir print(f"Конфигурационные файлы созданы успешно в директории {config_dir}") return True def start_api_server(): """Запускает API сервер""" print("Запуск API сервера...") # Устанавливаем переменные окружения api_env = os.environ.copy() api_env["TEN_AGENT_DIR"] = str(AGENTS_DIR) api_env["API_PORT"] = str(API_PORT) # Выводим информацию о директории с агентами для отладки print(f"Директория с агентами: {AGENTS_DIR}") print(f"Файлы в директории агентов:") if AGENTS_DIR.exists(): for file in AGENTS_DIR.iterdir(): if file.name.endswith('.json'): print(f" - {file.name} ({os.path.getsize(file)}b)") # В HuggingFace Space нужны дополнительные настройки if IS_HF_SPACE: print("Configuring API server for HuggingFace Space environment...") # Указываем серверу специально использовать API wrapper api_env["USE_WRAPPER"] = "true" # Отключаем логирование в файл api_env["TEN_LOG_DISABLE_FILE"] = "true" # Указываем путь для временных файлов api_env["TMP_DIR"] = "/tmp" # Запускаем Python API wrapper api_cmd = ["python", "api_wrapper.py"] print(f"Running API command: {' '.join(api_cmd)}") api_process = subprocess.Popen( api_cmd, env=api_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) # Ждем запуска сервера time.sleep(2) # Проверяем, что процесс не упал if api_process.poll() is not None: stdout, stderr = api_process.communicate() print(f"API сервер не запустился!") print(f"STDOUT: {stdout.decode()}") print(f"STDERR: {stderr.decode()}") return None print(f"API server started and listening on port {API_PORT}") # Запускаем поток для вывода логов def log_output(process, prefix): for line in iter(process.stdout.readline, b''): print(f"[{prefix}] {line.decode().strip()}") for line in iter(process.stderr.readline, b''): print(f"[{prefix} ERROR] {line.decode().strip()}") log_thread = threading.Thread(target=log_output, args=(api_process, "API")) log_thread.daemon = True log_thread.start() return api_process def start_playground(): """Запускает Playground UI через Next.js""" print("Запуск Playground UI...") # Создаем директорию для Next.js в /tmp, где у нас есть права на запись tmp_playground_dir = Path("/tmp/ten_playground") if not tmp_playground_dir.exists(): print(f"Создаем временную директорию для Next.js: {tmp_playground_dir}") tmp_playground_dir.mkdir(exist_ok=True, parents=True) # Копируем необходимые файлы из /app/playground в /tmp/ten_playground print("Копируем файлы Next.js приложения во временную директорию...") try: os.system(f"cp -r /app/playground/app /tmp/ten_playground/") os.system(f"cp -r /app/playground/public /tmp/ten_playground/") os.system(f"cp /app/playground/package.json /tmp/ten_playground/") os.system(f"cp /app/playground/next.config.mjs /tmp/ten_playground/") os.system(f"cp /app/playground/tailwind.config.js /tmp/ten_playground/") os.system(f"cp /app/playground/postcss.config.js /tmp/ten_playground/") # Проверяем, что файлы скопировались if not (tmp_playground_dir / "app").exists(): print("Не удалось скопировать файлы Next.js. Создаем простое приложение...") create_simple_next_app(tmp_playground_dir) except Exception as e: print(f"Ошибка при копировании файлов: {e}") print("Создаем простое приложение Next.js...") create_simple_next_app(tmp_playground_dir) # Устанавливаем переменные окружения ui_env = os.environ.copy() ui_env["PORT"] = str(NEXTJS_PORT) ui_env["AGENT_SERVER_URL"] = f"http://{INTERNAL_HOST}:{API_PORT}" ui_env["NEXT_PUBLIC_EDIT_GRAPH_MODE"] = "true" ui_env["NEXT_PUBLIC_DISABLE_CAMERA"] = "false" # В HuggingFace Space нужны дополнительные настройки if IS_HF_SPACE: print("Configuring for HuggingFace Space environment...") ui_env["NEXT_PUBLIC_IS_HF_SPACE"] = "true" # Отключаем строгие проверки CORS для работы в iframe ui_env["NEXT_PUBLIC_DISABLE_CORS"] = "true" # Запускаем UI из временной директории, где у нас есть права на запись ui_cmd = f"cd {tmp_playground_dir} && npx next dev" print(f"Running UI command: {ui_cmd}") ui_process = subprocess.Popen( ui_cmd, env=ui_env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) # Ждем запуска UI time.sleep(5) # Проверяем, что процесс не упал if ui_process.poll() is not None: stdout, stderr = ui_process.communicate() print(f"Playground UI не запустился!") print(f"STDOUT: {stdout.decode()}") print(f"STDERR: {stderr.decode()}") return None # Запускаем поток для вывода логов def log_output(process, prefix): for line in iter(process.stdout.readline, b''): print(f"[{prefix}] {line.decode().strip()}") for line in iter(process.stderr.readline, b''): print(f"[{prefix} ERROR] {line.decode().strip()}") log_thread = threading.Thread(target=log_output, args=(ui_process, "UI")) log_thread.daemon = True log_thread.start() return ui_process def create_interface(): """Создает Gradio интерфейс для редиректа""" with gr.Blocks() as demo: gr.Markdown("# TEN Agent на Hugging Face Space") gr.Markdown("## Управление и мониторинг") # Статус серверов status_md = gr.Markdown("### Статус: Инициализация...") with gr.Row(): col1, col2 = gr.Column(), gr.Column() with col1: # Информация об API сервере gr.Markdown(f""" ### API сервер API сервер работает по адресу: http://{INTERNAL_HOST}:{API_PORT} Доступные эндпоинты: - `/graphs` - Список доступных графов - `/health` - Статус API сервера """) # Кнопка для проверки API check_api_btn = gr.Button("Проверить API сервер") api_result = gr.JSON(label="Результат запроса к API") def check_api(): try: import requests response = requests.get(f"http://{INTERNAL_HOST}:{API_PORT}/health") return response.json() except Exception as e: return {"status": "error", "message": str(e)} check_api_btn.click(check_api, outputs=api_result) with col2: # Информация о UI сервере gr.Markdown(f""" ### UI сервер UI сервер доступен по адресу: {UI_URL} Открыть UI в новой вкладке """) # Функция для открытия UI в iframe iframe_btn = gr.Button("Показать UI в iframe") def show_iframe(): return f"""
""" iframe_area = gr.HTML() iframe_btn.click(show_iframe, outputs=iframe_area) # Ссылки на документацию и важные настройки with gr.Accordion("Инструкции", open=False): gr.Markdown(""" ### Важные настройки Для полноценной работы необходимо настроить следующие API ключи: 1. **OpenAI API Key** - для работы с языковыми моделями 2. **Deepgram API Key** - для распознавания речи 3. **ElevenLabs API Key** - для синтеза речи 4. **Agora App ID и Certificate** - для работы с RTC ### Доступные графы 1. **Voice Agent** - Голосовой агент с OpenAI и ElevenLabs 2. **Chat Agent** - Текстовый чат с OpenAI """) # Статус серверов и логи with gr.Accordion("Статус системы", open=False): api_status = gr.Textbox(label="Статус API сервера", value="Проверка...", interactive=False) ui_status = gr.Textbox(label="Статус UI сервера", value="Проверка...", interactive=False) # Функция обновления статуса def update_status(): api_status_msg = "✅ Активен" if is_port_in_use(API_PORT) else "❌ Не активен" ui_status_msg = "✅ Активен" if is_port_in_use(NEXTJS_PORT) else "❌ Не активен" status_md_msg = f"### Статус: {'✅ Все системы работают' if is_port_in_use(API_PORT) and is_port_in_use(NEXTJS_PORT) else '⚠️ Есть проблемы'}" return [api_status_msg, ui_status_msg, status_md_msg] status_btn = gr.Button("Обновить статус") status_btn.click(update_status, outputs=[api_status, ui_status, status_md]) # Обновляем статус при загрузке demo.load(update_status, outputs=[api_status, ui_status, status_md]) return demo # Вспомогательная функция для проверки, используется ли порт def is_port_in_use(port): import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: try: s.connect(('localhost', port)) return True except: return False def create_simple_next_app(target_dir): """Создает простое Next.js приложение в указанной директории""" print(f"Создание простого Next.js приложения в {target_dir}") # Создаем базовую структуру Next.js приложения app_dir = target_dir / "app" app_dir.mkdir(exist_ok=True, parents=True) # Создаем простой package.json package_json = { "name": "ten-agent-simple-ui", "version": "0.1.0", "private": True, "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }, "dependencies": { "next": "latest", "react": "latest", "react-dom": "latest" } } with open(target_dir / "package.json", "w") as f: json.dump(package_json, f, indent=2) # Создаем простой next.config.js next_config = """/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, } module.exports = nextConfig """ with open(target_dir / "next.config.js", "w") as f: f.write(next_config) # Создаем простую страницу page_content = """export default function Home() { return (

TEN Agent UI

API server is running at: http://localhost:8080

API endpoints:

); } """ with open(app_dir / "page.js", "w") as f: f.write(page_content) # Создаем простой layout.js layout_content = """export const metadata = { title: 'TEN Agent', description: 'Simple UI for TEN Agent', } export default function RootLayout({ children }) { return ( {children} ) } """ with open(app_dir / "layout.js", "w") as f: f.write(layout_content) print(f"Простое Next.js приложение создано в {target_dir}") def start_simple_ui(): """Запускает простой HTTP сервер для UI""" print("Запуск простого HTTP сервера...") # Создаем директорию для UI simple_ui_dir = Path("/tmp/ten_ui") simple_ui_dir.mkdir(exist_ok=True, parents=True) # Создаем простую HTML страницу html_content = """ TEN Agent Simple UI

TEN Agent UI

API Server

API сервер работает по адресу: http://localhost:8080

Доступные эндпоинты:

Запуск сессии

Для запуска сессии можно использовать API вручную:

curl -X POST http://localhost:8080/start -H "Content-Type: application/json" -d '{"graph_file":"voice_agent.json"}'
""" with open(simple_ui_dir / "index.html", "w") as f: f.write(html_content) # Запускаем простой HTTP сервер на порту 3000 server_cmd = f"cd {simple_ui_dir} && python -m http.server {NEXTJS_PORT}" print(f"Running simple HTTP server: {server_cmd}") server_process = subprocess.Popen( server_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) # Ждем запуска сервера time.sleep(2) # Проверяем, что процесс не упал if server_process.poll() is not None: stdout, stderr = server_process.communicate() print(f"Простой HTTP сервер не запустился!") print(f"STDOUT: {stdout.decode()}") print(f"STDERR: {stderr.decode()}") return None print(f"Простой HTTP сервер запущен и слушает на порту {NEXTJS_PORT}") return server_process def main(): # Создаем директории и файлы конфигурации create_directories() # Пытаемся создать конфигурационные файлы if not create_config_files(): print("ОШИБКА: Не удалось создать конфигурационные файлы!") return 1 # Запускаем API сервер api_process = start_api_server() if not api_process: print("Не удалось запустить API сервер") return 1 # Пробуем запустить Playground UI через Next.js ui_process = start_playground() # Если запуск через Next.js не удался, пробуем запустить простой HTTP сервер if not ui_process: print("Не удалось запустить Playground UI через Next.js, пробуем простой HTTP сервер...") ui_process = start_simple_ui() if not ui_process: print("Не удалось запустить ни один UI сервер. Продолжаем только с API сервером.") # Создаем Gradio интерфейс demo = create_interface() # Запускаем Gradio demo.launch(server_port=GRADIO_PORT, server_name=INTERNAL_HOST, share=False) # Остаемся в цикле до завершения процессов try: while True: if api_process.poll() is not None: print("API сервер остановлен") if ui_process and ui_process.poll() is None: ui_process.terminate() break if ui_process and ui_process.poll() is not None: print("UI сервер остановлен, пробуем перезапустить...") # Пробуем запустить простой HTTP сервер, если UI процесс упал ui_process = start_simple_ui() time.sleep(1) except KeyboardInterrupt: print("Принудительная остановка...") api_process.terminate() if ui_process: ui_process.terminate() 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())