#!/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 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): # Целевой API сервер 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 # Проксирование запроса к API 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 # Проксирование запроса к API 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: # Читаем графы из property.json 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): """Обработка запросов дизайнера""" # Новый формат ответа для designer API 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": [] } ] } # Читаем графы из property.json try: with open(PROPERTY_JSON, 'r') as f: property_data = json.load(f) graphs = property_data.get('graphs', []) # Обеспечиваем форматирование совместимое с designer API 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)}") # Тестируем разные варианты ответов # 1. Отправляем третий формат (простой массив графов) 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(): """Проверяет наличие всех необходимых файлов агентов и создает их при необходимости""" # Создаем manifest.json если он не существует 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} создан") # Создаем voice_agent.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} создан") # Создаем chat_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} байт)") # Если это JSON файл, выводим его содержимое 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)} графов") # Если API вернул пустой список, исправляем это 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) # Перезапускаем API сервер 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) # Проверяем и создаем property.json ensure_property_json() # Проверяем и создаем файлы агентов check_and_create_agent_files() # Проверка файлов перед запуском check_files() # Запускаем мок-сервер для API дизайнера 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() # Запускаем API сервер logger.info("Запуск TEN-Agent API сервера на порту 8080...") api_process = subprocess.Popen([str(API_BINARY)]) processes.append(api_process) # Тестируем API 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() # Запускаем Playground UI в режиме dev на порту 7860 (порт Hugging Face) logger.info("Запуск Playground UI в режиме разработки на порту 7860...") os.environ["PORT"] = "7860" os.environ["AGENT_SERVER_URL"] = f"http://localhost:{proxy_port}" # Указываем прокси вместо прямого API 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 UI 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())