#!/usr/bin/env python3 """ main.py — EzVibe AI 桌宠系统主入口 =================================== 用法: python main.py # 完整启动(PySide6 窗口 + 所有模块) python main.py --dummy # Dummy 模式(headless,无需 Qt) python main.py --server # 仅启动 API 服务器 依赖: pip install -r requirements.txt """ from __future__ import annotations import argparse import asyncio import atexit import os import signal import sys import threading from pathlib import Path # 确保项目根目录在 sys.path _ROOT = Path(__file__).parent.resolve() sys.path.insert(0, str(_ROOT)) # Linux: 必须在任何 Qt 模块导入之前设置平台 if sys.platform.startswith("linux") and os.environ.get("QT_QPA_PLATFORM") is None: os.environ["QT_QPA_PLATFORM"] = "xcb" # ============================================================================ # 组件导入 # ============================================================================ from agent.emotion import EmotionEngine from agent.memory import VectorMemory from agent.scheduler import BehaviorScheduler from perception import ( KeyboardMouseMonitor, ScreenCapture, get_global_monitor, stop_global_monitor, ) from ui.pet_window import create_pet_window, DummyPetWindow # brain 和 api 延迟导入(避免 dummy 模式下不必要的依赖) _brain_module = None _api_module = None def _get_brain(): global _brain_module if _brain_module is None: from agent import brain as _brain_module return _brain_module def _get_api(): global _api_module if _api_module is None: from api import server as _api_module return _api_module # ============================================================================ # EzVibeApp — 整合所有模块 # ============================================================================ class EzVibeApp: """ 桌宠应用主控制器。 负责初始化各层组件并协调生命周期: 1. 初始化情绪引擎(EmotionEngine) 2. 初始化记忆系统(VectorMemory) 3. 初始化 LLM 大脑(AgentBrain) 4. 初始化感知层(KeyboardMouseMonitor) 5. 初始化行为调度器(BehaviorScheduler) 6. 初始化桌宠窗口(PetWindow) 7. 启动 API 服务器(可选) 8. Qt 事件循环 + asyncio 并行运行 """ def __init__( self, llm_backend: str = "dummy", llm_config: dict | None = None, use_dummy_window: bool = False, start_api_server: bool = False, api_port: int = 8765, assets_dir: str | Path | None = None, ) -> None: self._llm_backend = llm_backend self._llm_config = llm_config or {} self._use_dummy = use_dummy_window self._start_api = start_api_server self._api_port = api_port self._assets_dir = assets_dir # ── 核心组件(延迟初始化) ───────────────────────────────────── self._emotion: EmotionEngine | None = None self._memory: VectorMemory | None = None self._brain = None self._scheduler: BehaviorScheduler | None = None self._monitor: KeyboardMouseMonitor | None = None self._screen: ScreenCapture | None = None self._window: DummyPetWindow | None = None self._api_server = None # ── 异步基础设施 ───────────────────────────────────────────── self._loop: asyncio.AbstractEventLoop | None = None self._api_thread: threading.Thread | None = None # 初始化 self._init_components() # ── 初始化 ──────────────────────────────────────────────────────────── def _init_components(self) -> None: """同步初始化所有组件。""" print("[EzVibe] 初始化组件 ...") # 1. 情绪引擎 self._emotion = EmotionEngine() print(f" [✓] EmotionEngine: {self._emotion.get_state()}") # 2. 记忆系统(embedder_backend="dummy" 避免依赖 Ollama) self._memory = VectorMemory( storage_path="data/MEMORY.db", embedder_backend="dummy", vector_mode="numpy", ) # VectorMemory.initialize() 是 async,但也可以同步调用 # 这里在同步初始化(因为我们还没有事件循环) try: import asyncio asyncio.run(self._memory.initialize()) except RuntimeError: # 如果已经在事件循环中,用 run_in_executor pass print(" [✓] VectorMemory initialized") # 3. LLM 大脑 brain_mod = _get_brain() self._brain = brain_mod.AgentBrain( llm_backend=self._llm_backend, llm_config=self._llm_config, emotion_engine=self._emotion, memory=self._memory, ) print(f" [✓] AgentBrain: {self._llm_backend}") # 4. 感知层 self._monitor = get_global_monitor( window_seconds=300.0, idle_timeout=60.0, ) self._screen = ScreenCapture() print(" [✓] KeyboardMouseMonitor started") # 5. 行为调度器 # BehaviorScheduler 内部有自己的 ActivityDetector, # 但 perception 层的 KeyboardMouseMonitor 也有一个。 # 这里让 scheduler 使用 monitor 自己的 _activity, # 需要确保接口兼容(perception ActivityDetector 有 activity_level @property)。 self._scheduler = BehaviorScheduler( emotion_engine=self._emotion, activity_detector=self._monitor._activity, ) print(" [✓] BehaviorScheduler initialized") # 6. 桌宠窗口(延迟创建,见 _run_qt) # 注意:PetWindow.__init__ 需要 QApplication,所以不能在这里调用 create_pet_window self._window = None # 注册退出清理 atexit.register(self.shutdown) # ── API 服务器(可选,后台线程) ────────────────────────────────────── def _start_api_server(self) -> None: """在后台线程启动 FastAPI 服务器。""" api_mod = _get_api() # 注意:APIServer 需要 emotion + brain self._api_server = api_mod.APIServer( emotion_engine=self._emotion, brain=self._brain, memory=self._memory, scheduler=self._scheduler, port=self._api_port, ) self._api_thread = threading.Thread( target=self._api_server.run, daemon=True, name="ezvibe-api", ) self._api_thread.start() print(f" [✓] APIServer started on http://127.0.0.1:{self._api_port}") # ── 行为调度集成 ────────────────────────────────────────────────────── def _run_scheduler_loop(self) -> None: """ 定期调用 scheduler.check_and_trigger()(async)。 每次触发时:显示提醒、记录到记忆。 """ import time while getattr(self, "_running", False): try: if self._scheduler and self._monitor: activity = self._monitor.get_activity() # check_and_trigger 是 async triggered: list[dict] = asyncio.run( self._scheduler.check_and_trigger(user_activity_level=activity) ) for action in triggered: # 显示提醒 self._window.show_reminder( action.get("message", ""), priority=action.get("priority", 1), ) # 记录到记忆(async add) try: asyncio.run( self._memory.add( text=f"[SCHEDULER] {action.get('type')}: {action.get('message', '')}", tags=["behavior", action.get("type", "unknown")], ) ) except Exception: pass except Exception as e: print(f"[EzVibe] Scheduler error: {e}") time.sleep(10) # 每 10 秒检查一次 # ── 主运行循环 ───────────────────────────────────────────────────────── def run(self) -> None: """启动应用。Dummy 模式和 Qt 模式共用同一套初始化。""" self._running = True print(f"\n[EzVibe] 启动中 ... (mode={'dummy' if self._use_dummy else 'full'})\n") # API 服务器(可选) if self._start_api: self._start_api_server() # 调度循环(后台线程) scheduler_thread = threading.Thread( target=self._run_scheduler_loop, daemon=True, name="ezvibe-scheduler", ) scheduler_thread.start() if self._use_dummy: self._run_dummy() else: self._run_qt() def _run_dummy(self) -> None: """Dummy 模式:显示状态摘要后退出。""" print("\n" + "=" * 50) print("EzVibe 桌宠 — Dummy 模式") print("=" * 50) print(f" 情绪状态 : {self._emotion.get_state()}") print(f" 活跃度 : {self._monitor.get_activity():.2f}") print(f" 调度器 : ready") # 记忆条目数 try: n_mem = self._memory._store.count() print(f" [✓] VectorMemory: {n_mem} entries") except Exception as e: print(f" [!] VectorMemory: {e}") print("=" * 50) print("\n模拟一次主动行为触发:") triggered: list[dict] = asyncio.run( self._scheduler.check_and_trigger(user_activity_level=0.05) ) if triggered: for action in triggered: print(f" → 触发行为: {action.get('type')} — {action.get('message', '')}") self._window.show_reminder(action.get("message", ""), priority=action.get("priority", 1)) else: print(" → 无行为触发(正常,取决于情绪和冷却状态)") print("\nDummy 模式完成。") print("要运行完整 GUI:python main.py") self.shutdown() def _run_qt(self) -> None: """Qt 模式:启动 PySide6/PyQt6 事件循环。""" try: from PySide6 import QtWidgets except ImportError: try: from PyQt6 import QtWidgets except ImportError: print("错误:需要 PySide6 或 PyQt6") print("安装:pip install PySide6") sys.exit(1) # Linux: 强制使用 XCB 平台插件(Wayland 兼容性问题) if sys.platform.startswith("linux") and os.environ.get("QT_QPA_PLATFORM") is None: os.environ["QT_QPA_PLATFORM"] = "xcb" app = QtWidgets.QApplication(sys.argv) app.setApplicationName("EzVibe") app.setApplicationDisplayName("EzVibe 桌宠") print(f"[EzVibe] Qt 平台: {app.platformName()}") # ★ 现在 QApplication 已存在,创建桌宠窗口(必须在 QApplication 之后) from ui.pet_window import create_pet_window self._window = create_pet_window( emotion_engine=self._emotion, brain=self._brain, scheduler=self._scheduler, assets_dir=self._assets_dir, force_dummy=self._use_dummy, ) if self._use_dummy: print(" [✓] PetWindow: DummyPetWindow (headless mode)") else: from ui.pet_window import _load_qt qt_lib = _load_qt() print(f" [✓] PetWindow: {type(self._window).__name__} (Qt={qt_lib})") # Ctrl-C (SIGINT) → 优雅退出 Qt 事件循环 def _sigint_handler(signum, frame): print("\n[EzVibe] 收到 Ctrl+C,正在关闭 ...") app.quit() signal.signal(signal.SIGINT, _sigint_handler) # 显示窗口 self._window.show() print(f"[EzVibe] 窗口 visible={self._window.isVisible()} geometry={self._window.geometry()}") # Qt 事件循环(主线程) # 同时嵌入 asyncio self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) def _run_async(): """每个 Qt 事件循环迭代运行一次 asyncio。""" try: self._loop.run_until_complete(asyncio.sleep(0)) except Exception: pass from PySide6.QtCore import QTimer timer = QTimer(app) timer.timeout.connect(_run_async) timer.setInterval(20) # 50 Hz timer.start() print("[EzVibe] 桌宠已显示。右键点击切换情绪。") print("[EzVibe] 按 Ctrl+C 或关闭窗口退出。") app.exec() self._running = False self.shutdown() # ── 生命周期 ────────────────────────────────────────────────────────── def shutdown(self) -> None: """优雅关闭所有组件。""" self._running = False print("\n[EzVibe] 正在关闭 ...") # 停止 monitor stop_global_monitor() # 停止窗口 if self._window: try: self._window.close() except Exception: pass # 关闭事件循环 if self._loop and self._loop.is_running(): self._loop.stop() print("[EzVibe] 已关闭。") # ============================================================================ # CLI 入口 # ============================================================================ def main(): parser = argparse.ArgumentParser( description="EzVibe AI 桌宠系统", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 示例: python main.py # 完整 GUI 模式 python main.py --dummy # Headless 测试 python main.py --server --port 8765 # 仅 API 服务器 python main.py --llm ollama # 使用 Ollama """, ) parser.add_argument( "--dummy", action="store_true", help="使用 DummyPetWindow(无需图形界面)" ) parser.add_argument( "--server", action="store_true", help="启动 FastAPI REST/WebSocket 服务器" ) parser.add_argument( "--port", type=int, default=8765, help="API 服务器端口(默认 8765)" ) parser.add_argument( "--llm", choices=["ollama", "openai", "dummy"], default="dummy", help="LLM 后端(默认 dummy)" ) parser.add_argument( "--assets", type=Path, default=None, help="桌宠图像资源目录" ) args = parser.parse_args() app = EzVibeApp( llm_backend=args.llm, use_dummy_window=args.dummy, start_api_server=args.server, api_port=args.port, assets_dir=args.assets, ) app.run() if __name__ == "__main__": main()