|
|
|
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 |
|
|
|
|
|
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)) |
|
|
|
|
|
if IS_HF_SPACE: |
|
|
|
UI_URL = f"https://{os.environ.get('SPACE_ID', 'unknown-space')}.hf.space/_next/iframe/3000" |
|
INTERNAL_HOST = "0.0.0.0" |
|
else: |
|
|
|
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_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 = { |
|
"_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 = { |
|
"_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) |
|
|
|
|
|
if IS_HF_SPACE: |
|
print("Configuring API server for HuggingFace Space environment...") |
|
|
|
api_env["USE_WRAPPER"] = "true" |
|
|
|
api_env["TEN_LOG_DISABLE_FILE"] = "true" |
|
|
|
api_env["TMP_DIR"] = str(TMP_DIR) |
|
|
|
|
|
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...") |
|
|
|
|
|
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) |
|
|
|
|
|
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" |
|
|
|
|
|
if IS_HF_SPACE: |
|
print("Configuring for HuggingFace Space environment...") |
|
ui_env["NEXT_PUBLIC_IS_HF_SPACE"] = "true" |
|
|
|
ui_env["NEXT_PUBLIC_DISABLE_CORS"] = "true" |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
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: |
|
|
|
gr.Markdown(f""" |
|
### API сервер |
|
|
|
API сервер работает по адресу: http://{INTERNAL_HOST}:{API_PORT} |
|
|
|
Доступные эндпоинты: |
|
- `/graphs` - Список доступных графов |
|
- `/health` - Статус 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: |
|
|
|
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> |
|
""") |
|
|
|
|
|
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}") |
|
|
|
|
|
app_dir = target_dir / "app" |
|
app_dir.mkdir(exist_ok=True, parents=True) |
|
|
|
|
|
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 = """/** @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_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 сервера...") |
|
|
|
|
|
simple_ui_dir = Path("/tmp/ten_ui") |
|
simple_ui_dir.mkdir(exist_ok=True, parents=True) |
|
|
|
|
|
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) |
|
|
|
|
|
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_process = start_api_server() |
|
if not api_process: |
|
print("Не удалось запустить API сервер") |
|
return |
|
|
|
|
|
ui_process = start_playground() |
|
|
|
|
|
if not ui_process: |
|
print("Не удалось запустить Playground UI через Next.js, пробуем простой HTTP сервер...") |
|
ui_process = start_simple_ui() |
|
|
|
if not ui_process: |
|
print("Не удалось запустить ни один UI сервер. Продолжаем только с API сервером.") |
|
|
|
|
|
demo = create_interface() |
|
|
|
|
|
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 сервер остановлен, пробуем перезапустить...") |
|
|
|
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()) |