ten / app.py
3v324v23's picture
Fix permissions issues on HuggingFace Space: Add fallback UI options
bbc9709
raw
history blame
26 kB
#!/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)
# Создаем пустой файл логов
(LOGS_DIR / "server.log").touch()
print(f"Директории созданы в {TMP_DIR}")
def create_config_files():
"""Создает базовые файлы конфигурации"""
print("Создание конфигурационных файлов...")
# Создаем 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"
}
]
}
with open(AGENTS_DIR / "property.json", "w") as f:
json.dump(property_data, f, indent=2)
# Создаем 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"
}
with open(AGENTS_DIR / "voice_agent.json", "w") as f:
json.dump(voice_agent, f, indent=2)
# Создаем 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"
}
with open(AGENTS_DIR / "chat_agent.json", "w") as f:
json.dump(chat_agent, f, indent=2)
print("Конфигурационные файлы созданы успешно")
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)
# В 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"] = str(TMP_DIR)
# Запускаем 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}
<a href="{UI_URL}" target="_blank" style="display: inline-block; padding: 10px 15px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 4px; margin: 10px 0;">Открыть UI в новой вкладке</a>
""")
# Функция для открытия UI в iframe
iframe_btn = gr.Button("Показать UI в iframe")
def show_iframe():
return f"""
<div style="border: 1px solid #ccc; padding: 10px; border-radius: 5px;">
<iframe src="{UI_URL}" width="100%" height="500px" frameborder="0"></iframe>
</div>
"""
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 (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1>TEN Agent UI</h1>
<p>API server is running at: <a href="http://localhost:8080">http://localhost:8080</a></p>
<div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f0f0f0', borderRadius: '5px' }}>
<p>API endpoints:</p>
<ul>
<li><a href="http://localhost:8080/graphs">/graphs</a> - Available graphs</li>
<li><a href="http://localhost:8080/health">/health</a> - API server status</li>
</ul>
</div>
<div style={{ marginTop: '20px' }}>
<button
style={{
padding: '10px 15px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
onClick={() => window.location.href = 'http://localhost:8080/graphs'}
>
Go to API
</button>
</div>
</div>
);
}
"""
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 (
<html lang="en">
<body>{children}</body>
</html>
)
}
"""
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 = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TEN Agent Simple UI</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.card {
background-color: #f5f5f5;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 10px 15px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
}
a {
color: #0066cc;
}
</style>
</head>
<body>
<h1>TEN Agent UI</h1>
<div class="card">
<h2>API Server</h2>
<p>API сервер работает по адресу: <a href="http://localhost:8080" target="_blank">http://localhost:8080</a></p>
<p>Доступные эндпоинты:</p>
<ul>
<li><a href="http://localhost:8080/graphs" target="_blank">/graphs</a> - Список доступных графов</li>
<li><a href="http://localhost:8080/health" target="_blank">/health</a> - Статус API сервера</li>
</ul>
</div>
<div class="card">
<h2>Запуск сессии</h2>
<p>Для запуска сессии можно использовать API вручную:</p>
<pre>curl -X POST http://localhost:8080/start -H "Content-Type: application/json" -d '{"graph_file":"voice_agent.json"}'</pre>
</div>
</body>
</html>
"""
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()
create_config_files()
# Запускаем API сервер
api_process = start_api_server()
if not api_process:
print("Не удалось запустить API сервер")
return
# Пробуем запустить 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()
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())