|
|
|
import os |
|
import subprocess |
|
import sys |
|
import time |
|
import json |
|
from pathlib import Path |
|
import signal |
|
import threading |
|
import shutil |
|
import logging |
|
import urllib.request |
|
import urllib.error |
|
import tempfile |
|
import http.server |
|
import socketserver |
|
import requests |
|
from threading import Thread |
|
from http import HTTPStatus |
|
from typing import Dict, Any, Optional |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, |
|
format='%(asctime)s [%(levelname)s] %(message)s', |
|
datefmt='%Y-%m-%d %H:%M:%S') |
|
|
|
logger = logging.getLogger('ten-agent') |
|
|
|
|
|
AGENTS_DIR = Path("/app/agents") |
|
PROPERTY_JSON = AGENTS_DIR / "property.json" |
|
MANIFEST_JSON = AGENTS_DIR / "manifest.json" |
|
VOICE_AGENT_JSON = AGENTS_DIR / "voice_agent.json" |
|
CHAT_AGENT_JSON = AGENTS_DIR / "chat_agent.json" |
|
API_BINARY = Path("/app/server/bin/api") |
|
PLAYGROUND_DIR = Path("/app/playground") |
|
BACKUP_DIR = Path("/app/backup") |
|
MOCK_API_DIR = Path("/app/mock-api") |
|
|
|
|
|
DESIGNER_PACKAGES_JSON = MOCK_API_DIR / "designer-packages.json" |
|
|
|
|
|
mock_responses = {} |
|
|
|
def load_mock_responses(): |
|
"""Загружает предварительно созданные мок-ответы""" |
|
global mock_responses |
|
|
|
try: |
|
if DESIGNER_PACKAGES_JSON.exists(): |
|
with open(DESIGNER_PACKAGES_JSON, 'r') as f: |
|
mock_responses['packages'] = json.load(f) |
|
logger.info(f"Загружен мок-ответ для дизайнера: {mock_responses['packages']}") |
|
except Exception as e: |
|
logger.error(f"Ошибка при загрузке мок-ответов: {e}") |
|
|
|
mock_responses['packages'] = { |
|
"success": True, |
|
"packages": [ |
|
{ |
|
"name": "default", |
|
"description": "Default package", |
|
"graphs": [ |
|
{ |
|
"name": "Voice Agent", |
|
"description": "Voice Agent with OpenAI", |
|
"file": "voice_agent.json", |
|
"id": "voice_agent", |
|
"package": "default" |
|
}, |
|
{ |
|
"name": "Chat Agent", |
|
"description": "Chat Agent", |
|
"file": "chat_agent.json", |
|
"id": "chat_agent", |
|
"package": "default" |
|
} |
|
] |
|
} |
|
] |
|
} |
|
|
|
class ProxyHTTPRequestHandler(http.server.BaseHTTPRequestHandler): |
|
"""HTTP-прокси для отладки и исправления запросов между UI и API""" |
|
|
|
def __init__(self, *args, **kwargs): |
|
|
|
self.api_host = "localhost" |
|
self.api_port = 8080 |
|
super().__init__(*args, **kwargs) |
|
|
|
def do_GET(self): |
|
"""Обработка GET запросов""" |
|
logger.info(f"PROXY: GET запрос: {self.path}") |
|
|
|
|
|
if self.path == "/graphs" or self.path == "/api/agents/graphs": |
|
self._handle_graphs_request() |
|
return |
|
|
|
|
|
target_url = f"http://{self.api_host}:{self.api_port}{self.path}" |
|
try: |
|
response = requests.get(target_url, headers=self._get_headers()) |
|
self._send_response(response) |
|
except Exception as e: |
|
logger.error(f"PROXY: Ошибка при проксировании GET-запроса: {e}") |
|
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) |
|
|
|
def do_POST(self): |
|
"""Обработка POST запросов""" |
|
content_length = int(self.headers.get('Content-Length', 0)) |
|
post_data = self.rfile.read(content_length) if content_length > 0 else b'' |
|
|
|
logger.info(f"PROXY: POST запрос: {self.path}") |
|
|
|
|
|
if "/api/designer/v1/packages/reload" in self.path or "/api/dev/v1/packages/reload" in self.path: |
|
self._handle_designer_reload() |
|
return |
|
|
|
|
|
target_url = f"http://{self.api_host}:{self.api_port}{self.path}" |
|
try: |
|
response = requests.post(target_url, data=post_data, headers=self._get_headers()) |
|
self._send_response(response) |
|
except Exception as e: |
|
logger.error(f"PROXY: Ошибка при проксировании POST-запроса: {e}") |
|
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) |
|
|
|
def _handle_graphs_request(self): |
|
"""Специальная обработка для запроса графов""" |
|
try: |
|
|
|
with open(PROPERTY_JSON, 'r') as f: |
|
property_data = json.load(f) |
|
|
|
graphs = property_data.get('graphs', []) |
|
logger.info(f"PROXY: Возвращаем графы напрямую: {json.dumps(graphs)}") |
|
|
|
self.send_response(HTTPStatus.OK) |
|
self.send_header('Content-Type', 'application/json') |
|
self.end_headers() |
|
self.wfile.write(json.dumps(graphs).encode('utf-8')) |
|
except Exception as e: |
|
logger.error(f"PROXY: Ошибка при обработке запроса графов: {e}") |
|
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) |
|
|
|
def _handle_designer_reload(self): |
|
"""Обработка запросов дизайнера""" |
|
|
|
response_data = { |
|
"status": 200, |
|
"packages": [ |
|
{ |
|
"name": "default", |
|
"description": "Default package", |
|
"agents": [ |
|
{ |
|
"name": "voice_agent", |
|
"description": "A simple voice agent" |
|
}, |
|
{ |
|
"name": "chat_agent", |
|
"description": "A simple chat agent" |
|
} |
|
], |
|
"graphs": [] |
|
} |
|
] |
|
} |
|
|
|
|
|
try: |
|
with open(PROPERTY_JSON, 'r') as f: |
|
property_data = json.load(f) |
|
|
|
graphs = property_data.get('graphs', []) |
|
|
|
formatted_graphs = [] |
|
for graph in graphs: |
|
|
|
graph_copy = graph.copy() |
|
if 'id' not in graph_copy: |
|
graph_copy['id'] = graph_copy.get('name', '').lower().replace(' ', '_') |
|
if 'package' not in graph_copy: |
|
graph_copy['package'] = 'default' |
|
formatted_graphs.append(graph_copy) |
|
|
|
response_data["packages"][0]["graphs"] = formatted_graphs |
|
|
|
|
|
alternative_format = { |
|
"success": True, |
|
"packages": [ |
|
{ |
|
"name": "default", |
|
"description": "Default package", |
|
"graphs": formatted_graphs |
|
} |
|
] |
|
} |
|
|
|
alternate_format2 = formatted_graphs |
|
|
|
logger.info(f"PROXY: Ответ для designer API, формат 1: {json.dumps(response_data)}") |
|
logger.info(f"PROXY: Ответ для designer API, формат 2: {json.dumps(alternative_format)}") |
|
logger.info(f"PROXY: Ответ для designer API, формат 3: {json.dumps(alternate_format2)}") |
|
|
|
|
|
|
|
self.send_response(HTTPStatus.OK) |
|
self.send_header('Content-Type', 'application/json') |
|
self.end_headers() |
|
self.wfile.write(json.dumps(alternate_format2).encode('utf-8')) |
|
|
|
|
|
|
|
except Exception as e: |
|
logger.error(f"PROXY: Ошибка при чтении графов для дизайнера: {e}") |
|
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) |
|
|
|
def _get_headers(self): |
|
"""Получение заголовков для проксирования""" |
|
headers = {} |
|
for header in self.headers: |
|
headers[header] = self.headers[header] |
|
return headers |
|
|
|
def _send_response(self, response): |
|
"""Отправка ответа клиенту""" |
|
self.send_response(response.status_code) |
|
|
|
|
|
for header, value in response.headers.items(): |
|
self.send_header(header, value) |
|
self.end_headers() |
|
|
|
|
|
self.wfile.write(response.content) |
|
|
|
def log_message(self, format, *args): |
|
"""Переопределение логирования""" |
|
logger.debug(f"PROXY: {self.address_string()} - {format % args}") |
|
|
|
class MockDesignerAPIHandler(http.server.BaseHTTPRequestHandler): |
|
"""Обработчик для мок-API дизайнера""" |
|
|
|
def do_GET(self): |
|
"""Обработка GET запросов""" |
|
logger.info(f"MOCK API: GET запрос: {self.path}") |
|
|
|
if self.path == "/api/designer/v1/packages" or self.path == "/api/dev/v1/packages": |
|
self._send_designer_packages() |
|
elif self.path.startswith("/api/designer/v1/") or self.path.startswith("/api/dev/v1/"): |
|
self._send_success_response({"success": True}) |
|
else: |
|
self.send_error(404, "Endpoint not found") |
|
|
|
def do_POST(self): |
|
"""Обработка POST запросов""" |
|
logger.info(f"MOCK API: POST запрос: {self.path}") |
|
|
|
if self.path == "/api/designer/v1/packages/reload" or self.path == "/api/dev/v1/packages/reload": |
|
self._send_designer_packages() |
|
elif self.path.startswith("/api/designer/v1/") or self.path.startswith("/api/dev/v1/"): |
|
self._send_success_response({"success": True}) |
|
else: |
|
self.send_error(404, "Endpoint not found") |
|
|
|
def _send_designer_packages(self): |
|
"""Отправляет информацию о пакетах и графах""" |
|
self._send_success_response(mock_responses['packages']) |
|
|
|
def _send_success_response(self, data: Dict[str, Any]): |
|
"""Отправляет успешный ответ с данными""" |
|
self.send_response(200) |
|
self.send_header('Content-Type', 'application/json') |
|
self.end_headers() |
|
self.wfile.write(json.dumps(data).encode('utf-8')) |
|
|
|
def log_message(self, format, *args): |
|
"""Настраиваем логирование для сервера""" |
|
logger.debug(f"MOCK API: {self.address_string()} - {format % args}") |
|
|
|
def run_proxy_server(port=9090): |
|
"""Запуск прокси-сервера""" |
|
try: |
|
with socketserver.TCPServer(("", port), ProxyHTTPRequestHandler) as httpd: |
|
logger.info(f"Запуск прокси-сервера на порту {port}") |
|
httpd.serve_forever() |
|
except Exception as e: |
|
logger.error(f"Ошибка при запуске прокси-сервера: {e}") |
|
|
|
def ensure_directory_permissions(directory_path): |
|
"""Обеспечиваем правильные разрешения для директории""" |
|
directory = Path(directory_path) |
|
if not directory.exists(): |
|
logger.info(f"Создание директории {directory}") |
|
directory.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
subprocess.run(["chmod", "-R", "777", str(directory)]) |
|
logger.info(f"Права доступа для {directory} установлены") |
|
|
|
def backup_file(filepath): |
|
"""Создает резервную копию файла""" |
|
src_path = Path(filepath) |
|
if not src_path.exists(): |
|
logger.warning(f"Невозможно создать резервную копию: {filepath} не существует") |
|
return |
|
|
|
BACKUP_DIR.mkdir(parents=True, exist_ok=True) |
|
dest_path = BACKUP_DIR / f"{src_path.name}.bak" |
|
|
|
try: |
|
shutil.copy2(src_path, dest_path) |
|
logger.info(f"Резервная копия создана: {dest_path}") |
|
except Exception as e: |
|
logger.error(f"Ошибка при создании резервной копии {filepath}: {e}") |
|
|
|
def ensure_property_json(): |
|
"""Проверяет и создает property.json при необходимости""" |
|
try: |
|
if not PROPERTY_JSON.exists(): |
|
logger.warning(f"{PROPERTY_JSON} не найден, создаем файл...") |
|
|
|
property_data = { |
|
"_ten": {}, |
|
"name": "TEN Agent Example", |
|
"version": "0.0.1", |
|
"extensions": ["openai_chatgpt"], |
|
"description": "A basic voice agent with OpenAI", |
|
"graphs": [ |
|
{ |
|
"name": "Voice Agent", |
|
"description": "Basic voice agent with OpenAI", |
|
"file": "voice_agent.json" |
|
}, |
|
{ |
|
"name": "Chat Agent", |
|
"description": "Simple chat agent", |
|
"file": "chat_agent.json" |
|
} |
|
] |
|
} |
|
|
|
PROPERTY_JSON.parent.mkdir(parents=True, exist_ok=True) |
|
|
|
with open(PROPERTY_JSON, 'w') as f: |
|
json.dump(property_data, f, indent=2) |
|
|
|
logger.info(f"Файл {PROPERTY_JSON} создан успешно") |
|
except Exception as e: |
|
logger.error(f"Ошибка при создании {PROPERTY_JSON}: {e}") |
|
|
|
def check_and_create_agent_files(): |
|
"""Проверяет наличие всех необходимых файлов агентов и создает их при необходимости""" |
|
|
|
|
|
if not MANIFEST_JSON.exists(): |
|
manifest_data = { |
|
"name": "default", |
|
"agents": [ |
|
{ |
|
"name": "voice_agent", |
|
"description": "A simple voice agent" |
|
}, |
|
{ |
|
"name": "chat_agent", |
|
"description": "A text chat agent" |
|
} |
|
] |
|
} |
|
|
|
with open(MANIFEST_JSON, 'w') as f: |
|
json.dump(manifest_data, f, indent=2) |
|
os.chmod(MANIFEST_JSON, 0o666) |
|
logger.info(f"Файл {MANIFEST_JSON} создан") |
|
|
|
|
|
if not VOICE_AGENT_JSON.exists(): |
|
voice_agent_data = { |
|
"nodes": [], |
|
"edges": [], |
|
"groups": [], |
|
"templates": [], |
|
"root": None |
|
} |
|
|
|
with open(VOICE_AGENT_JSON, 'w') as f: |
|
json.dump(voice_agent_data, f, indent=2) |
|
os.chmod(VOICE_AGENT_JSON, 0o666) |
|
logger.info(f"Файл {VOICE_AGENT_JSON} создан") |
|
|
|
|
|
if not CHAT_AGENT_JSON.exists(): |
|
chat_agent_data = { |
|
"nodes": [], |
|
"edges": [], |
|
"groups": [], |
|
"templates": [], |
|
"root": None |
|
} |
|
|
|
with open(CHAT_AGENT_JSON, 'w') as f: |
|
json.dump(chat_agent_data, f, indent=2) |
|
os.chmod(CHAT_AGENT_JSON, 0o666) |
|
logger.info(f"Файл {CHAT_AGENT_JSON} создан") |
|
|
|
def check_files(): |
|
"""Проверяет и выводит информацию о важных файлах""" |
|
files_to_check = [ |
|
PROPERTY_JSON, |
|
MANIFEST_JSON, |
|
VOICE_AGENT_JSON, |
|
CHAT_AGENT_JSON, |
|
API_BINARY |
|
] |
|
|
|
logger.info("=== Проверка критических файлов ===") |
|
for file_path in files_to_check: |
|
path = Path(file_path) |
|
if path.exists(): |
|
if path.is_file(): |
|
size = path.stat().st_size |
|
logger.info(f"✅ {file_path} (размер: {size} байт)") |
|
|
|
|
|
if str(file_path).endswith('.json'): |
|
try: |
|
with open(file_path, 'r') as f: |
|
content = json.load(f) |
|
logger.info(f" Содержимое: {json.dumps(content, indent=2)}") |
|
except Exception as e: |
|
logger.error(f" Ошибка чтения JSON: {e}") |
|
else: |
|
logger.warning(f"❌ {file_path} (это директория, а не файл)") |
|
else: |
|
logger.error(f"❌ {file_path} (файл не найден)") |
|
|
|
logger.info("=== Проверка структуры директорий ===") |
|
logger.info(f"Содержимое {AGENTS_DIR}:") |
|
subprocess.run(["ls", "-la", str(AGENTS_DIR)]) |
|
|
|
logger.info("Проверка прав доступа:") |
|
subprocess.run(["stat", str(AGENTS_DIR)]) |
|
subprocess.run(["stat", str(PROPERTY_JSON)]) |
|
|
|
def test_api(): |
|
"""Делает запрос к API для получения списка графов""" |
|
logger.info("=== Тестирование API ===") |
|
try: |
|
|
|
time.sleep(3) |
|
with urllib.request.urlopen("http://localhost:8080/graphs") as response: |
|
data = response.read().decode('utf-8') |
|
logger.info(f"Ответ /graphs: {data}") |
|
|
|
|
|
try: |
|
json_data = json.loads(data) |
|
if isinstance(json_data, list) and len(json_data) > 0: |
|
logger.info(f"API вернул {len(json_data)} графов") |
|
|
|
if len(json_data) == 0: |
|
logger.warning("API вернул пустой список графов, исправляем property.json") |
|
backup_file(PROPERTY_JSON) |
|
ensure_property_json() |
|
check_and_create_agent_files() |
|
ensure_directory_permissions(AGENTS_DIR) |
|
|
|
logger.info("Перезапускаем API сервер...") |
|
subprocess.run(["pkill", "-f", str(API_BINARY)]) |
|
time.sleep(1) |
|
subprocess.Popen([str(API_BINARY)]) |
|
else: |
|
logger.warning("API вернул пустой список графов") |
|
except json.JSONDecodeError: |
|
logger.error("Ответ API не является валидным JSON") |
|
except urllib.error.URLError as e: |
|
logger.error(f"Ошибка запроса к API: {e}") |
|
except Exception as e: |
|
logger.error(f"Неизвестная ошибка при запросе к API: {e}") |
|
|
|
def run_mock_api_server(port=8090): |
|
"""Запускает мок-сервер для API дизайнера""" |
|
try: |
|
with socketserver.TCPServer(("", port), MockDesignerAPIHandler) as httpd: |
|
logger.info(f"Запуск мок-API сервера на порту {port}") |
|
httpd.serve_forever() |
|
except Exception as e: |
|
logger.error(f"Ошибка при запуске мок-API сервера: {e}") |
|
|
|
def main(): |
|
processes = [] |
|
try: |
|
|
|
load_mock_responses() |
|
|
|
|
|
if not API_BINARY.exists(): |
|
logger.error(f"API binary не найден: {API_BINARY}") |
|
return 1 |
|
|
|
if not PLAYGROUND_DIR.exists(): |
|
logger.error(f"Playground директория не найдена: {PLAYGROUND_DIR}") |
|
return 1 |
|
|
|
|
|
ensure_directory_permissions(AGENTS_DIR) |
|
ensure_directory_permissions(BACKUP_DIR) |
|
|
|
|
|
ensure_property_json() |
|
|
|
|
|
check_and_create_agent_files() |
|
|
|
|
|
check_files() |
|
|
|
|
|
mock_api_port = 8090 |
|
mock_api_thread = threading.Thread(target=run_mock_api_server, args=(mock_api_port,)) |
|
mock_api_thread.daemon = True |
|
mock_api_thread.start() |
|
|
|
|
|
logger.info("Запуск TEN-Agent API сервера на порту 8080...") |
|
api_process = subprocess.Popen([str(API_BINARY)]) |
|
processes.append(api_process) |
|
|
|
|
|
test_thread = threading.Thread(target=test_api) |
|
test_thread.daemon = True |
|
test_thread.start() |
|
|
|
|
|
proxy_port = 9090 |
|
proxy_thread = Thread(target=run_proxy_server, args=(proxy_port,)) |
|
proxy_thread.daemon = True |
|
proxy_thread.start() |
|
|
|
|
|
logger.info("Запуск Playground UI в режиме разработки на порту 7860...") |
|
os.environ["PORT"] = "7860" |
|
os.environ["AGENT_SERVER_URL"] = f"http://localhost:{proxy_port}" |
|
os.environ["NEXT_PUBLIC_EDIT_GRAPH_MODE"] = "true" |
|
os.environ["NEXT_PUBLIC_DISABLE_CAMERA"] = "true" |
|
|
|
|
|
os.environ["NEXT_PUBLIC_DEV_MODE"] = "true" |
|
os.environ["NEXT_PUBLIC_API_BASE_URL"] = "/api/agents" |
|
os.environ["NEXT_PUBLIC_DESIGNER_API_URL"] = f"http://localhost:{mock_api_port}" |
|
|
|
|
|
playground_process = subprocess.Popen( |
|
["pnpm", "dev"], |
|
cwd=str(PLAYGROUND_DIR), |
|
env=os.environ |
|
) |
|
processes.append(playground_process) |
|
|
|
|
|
for proc in processes: |
|
proc.wait() |
|
|
|
except KeyboardInterrupt: |
|
logger.info("Завершение работы...") |
|
except Exception as e: |
|
logger.error(f"Ошибка: {e}") |
|
finally: |
|
|
|
for proc in processes: |
|
if proc and proc.poll() is None: |
|
proc.terminate() |
|
try: |
|
proc.wait(timeout=5) |
|
except subprocess.TimeoutExpired: |
|
proc.kill() |
|
|
|
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()) |