# -*- coding: utf-8 -*- """Runtime/workspace/skills handlers extracted from the main Gateway module. Deprecated note: Agent/workspace/skill read-write operations are being migrated to agent_service REST endpoints. These websocket handlers remain as a compatibility fallback and should not be considered the primary control plane path for frontend reads/writes. """ from __future__ import annotations import json from datetime import datetime from typing import Any from backend.agents.agent_workspace import load_agent_workspace_config from backend.agents.skills_manager import SkillsManager from backend.agents.toolkit_factory import load_agent_profiles from backend.config.bootstrap_config import ( get_bootstrap_config_for_run, resolve_runtime_config, update_bootstrap_values_for_run, ) from backend.llm.models import get_agent_model_info async def handle_reload_runtime_assets(gateway: Any) -> None: config_name = gateway.config.get("config_name", "default") runtime_config = resolve_runtime_config( project_root=gateway._project_root, config_name=config_name, enable_memory=gateway.config.get("enable_memory", False), schedule_mode=gateway.config.get("schedule_mode", "daily"), interval_minutes=gateway.config.get("interval_minutes", 60), trigger_time=gateway.config.get("trigger_time", "09:30"), ) result = gateway.pipeline.reload_runtime_assets(runtime_config=runtime_config) runtime_updates = gateway._apply_runtime_config(runtime_config) await gateway.state_sync.on_system_message("Runtime assets reloaded.") await gateway.broadcast({"type": "runtime_assets_reloaded", **result, **runtime_updates}) async def handle_update_runtime_config(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: updates: dict[str, Any] = {} schedule_mode = str(data.get("schedule_mode", "")).strip().lower() if schedule_mode: if schedule_mode not in {"daily", "intraday"}: await websocket.send(json.dumps({"type": "error", "message": "schedule_mode must be 'daily' or 'intraday'."}, ensure_ascii=False)) return updates["schedule_mode"] = schedule_mode interval_minutes = data.get("interval_minutes") if interval_minutes is not None: try: parsed_interval = int(interval_minutes) except (TypeError, ValueError): parsed_interval = 0 if parsed_interval <= 0: await websocket.send(json.dumps({"type": "error", "message": "interval_minutes must be a positive integer."}, ensure_ascii=False)) return updates["interval_minutes"] = parsed_interval trigger_time = data.get("trigger_time") if trigger_time is not None: raw_trigger = str(trigger_time).strip() if raw_trigger and raw_trigger != "now": try: datetime.strptime(raw_trigger, "%H:%M") except ValueError: await websocket.send(json.dumps({"type": "error", "message": "trigger_time must use HH:MM or 'now'."}, ensure_ascii=False)) return updates["trigger_time"] = raw_trigger or "09:30" max_comm_cycles = data.get("max_comm_cycles") if max_comm_cycles is not None: try: parsed_cycles = int(max_comm_cycles) except (TypeError, ValueError): parsed_cycles = 0 if parsed_cycles <= 0: await websocket.send(json.dumps({"type": "error", "message": "max_comm_cycles must be a positive integer."}, ensure_ascii=False)) return updates["max_comm_cycles"] = parsed_cycles initial_cash = data.get("initial_cash") if initial_cash is not None: try: parsed_initial_cash = float(initial_cash) except (TypeError, ValueError): parsed_initial_cash = 0.0 if parsed_initial_cash <= 0: await websocket.send(json.dumps({"type": "error", "message": "initial_cash must be a positive number."}, ensure_ascii=False)) return updates["initial_cash"] = parsed_initial_cash margin_requirement = data.get("margin_requirement") if margin_requirement is not None: try: parsed_margin_requirement = float(margin_requirement) except (TypeError, ValueError): parsed_margin_requirement = -1.0 if parsed_margin_requirement < 0: await websocket.send(json.dumps({"type": "error", "message": "margin_requirement must be a non-negative number."}, ensure_ascii=False)) return updates["margin_requirement"] = parsed_margin_requirement enable_memory = data.get("enable_memory") if enable_memory is not None: updates["enable_memory"] = bool(enable_memory) if not updates: await websocket.send(json.dumps({"type": "error", "message": "No runtime settings were provided."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") update_bootstrap_values_for_run( project_root=gateway._project_root, config_name=config_name, updates=updates, ) await gateway.state_sync.on_system_message("运行时调度配置已保存,正在热更新") await handle_reload_runtime_assets(gateway) async def handle_update_watchlist(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: tickers = gateway._normalize_watchlist(data.get("tickers")) if not tickers: await websocket.send(json.dumps({"type": "error", "message": "update_watchlist requires at least one valid ticker."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") update_bootstrap_values_for_run( project_root=gateway._project_root, config_name=config_name, updates={"tickers": tickers}, ) await gateway.state_sync.on_system_message(f"Watchlist updated: {', '.join(tickers)}") await gateway.broadcast({"type": "watchlist_updated", "config_name": config_name, "tickers": tickers}) await handle_reload_runtime_assets(gateway) gateway._schedule_watchlist_market_store_refresh(tickers) async def handle_get_agent_skills(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() if not agent_id: await websocket.send(json.dumps({"type": "error", "message": "get_agent_skills requires agent_id."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) agent_asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id) agent_config = load_agent_workspace_config(agent_asset_dir / "agent.yaml") resolved_skills = set(skills_manager.resolve_agent_skill_names(config_name=config_name, agent_id=agent_id, default_skills=[])) enabled = set(agent_config.enabled_skills) disabled = set(agent_config.disabled_skills) payload = [] for item in skills_manager.list_agent_skill_catalog(config_name, agent_id): if item.skill_name in disabled: status = "disabled" elif item.skill_name in enabled: status = "enabled" elif item.skill_name in resolved_skills: status = "active" else: status = "available" payload.append({ "skill_name": item.skill_name, "name": item.name, "description": item.description, "version": item.version, "source": item.source, "tools": item.tools, "status": status, }) await websocket.send(json.dumps({ "type": "agent_skills_loaded", "config_name": config_name, "agent_id": agent_id, "skills": payload, }, ensure_ascii=False)) async def handle_get_agent_profile(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() if not agent_id: await websocket.send(json.dumps({"type": "error", "message": "get_agent_profile requires agent_id."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id) agent_config = load_agent_workspace_config(asset_dir / "agent.yaml") profiles = load_agent_profiles() profile = profiles.get(agent_id, {}) bootstrap = get_bootstrap_config_for_run(gateway._project_root, config_name) override = bootstrap.agent_override(agent_id) active_tool_groups = override.get("active_tool_groups", agent_config.active_tool_groups or profile.get("active_tool_groups", [])) if not isinstance(active_tool_groups, list): active_tool_groups = [] disabled_tool_groups = agent_config.disabled_tool_groups if disabled_tool_groups: disabled_set = set(disabled_tool_groups) active_tool_groups = [group_name for group_name in active_tool_groups if group_name not in disabled_set] default_skills = profile.get("skills", []) if not isinstance(default_skills, list): default_skills = [] resolved_skills = skills_manager.resolve_agent_skill_names( config_name=config_name, agent_id=agent_id, default_skills=default_skills, ) prompt_files = agent_config.prompt_files or ["SOUL.md", "PROFILE.md", "AGENTS.md", "POLICY.md", "MEMORY.md"] model_name, model_provider = get_agent_model_info(agent_id) await websocket.send(json.dumps({ "type": "agent_profile_loaded", "config_name": config_name, "agent_id": agent_id, "profile": { "model_name": model_name, "model_provider": model_provider, "prompt_files": prompt_files, "default_skills": default_skills, "resolved_skills": resolved_skills, "active_tool_groups": active_tool_groups, "disabled_tool_groups": disabled_tool_groups, "enabled_skills": agent_config.enabled_skills, "disabled_skills": agent_config.disabled_skills, }, }, ensure_ascii=False)) async def handle_get_skill_detail(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() skill_name = str(data.get("skill_name", "")).strip() if not skill_name: await websocket.send(json.dumps({"type": "error", "message": "get_skill_detail requires skill_name."}, ensure_ascii=False)) return skills_manager = SkillsManager(project_root=gateway._project_root) try: if agent_id: config_name = gateway.config.get("config_name", "default") detail = skills_manager.load_agent_skill_document(config_name=config_name, agent_id=agent_id, skill_name=skill_name) else: detail = skills_manager.load_skill_document(skill_name) except FileNotFoundError: await websocket.send(json.dumps({"type": "error", "message": f"Unknown skill: {skill_name}"}, ensure_ascii=False)) return await websocket.send(json.dumps({ "type": "skill_detail_loaded", "agent_id": agent_id, "skill": detail, }, ensure_ascii=False)) async def handle_create_agent_local_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() skill_name = str(data.get("skill_name", "")).strip() if not agent_id or not skill_name: await websocket.send(json.dumps({"type": "error", "message": "create_agent_local_skill requires agent_id and skill_name."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) try: skills_manager.create_agent_local_skill(config_name=config_name, agent_id=agent_id, skill_name=skill_name) except (ValueError, FileExistsError) as exc: await websocket.send(json.dumps({"type": "error", "message": str(exc)}, ensure_ascii=False)) return await gateway.state_sync.on_system_message(f"Created local skill {skill_name} for {agent_id}") await gateway._handle_reload_runtime_assets() await websocket.send(json.dumps({"type": "agent_local_skill_created", "agent_id": agent_id, "skill_name": skill_name}, ensure_ascii=False)) await handle_get_agent_skills(gateway, websocket, {"agent_id": agent_id}) await handle_get_skill_detail(gateway, websocket, {"agent_id": agent_id, "skill_name": skill_name}) async def handle_update_agent_local_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() skill_name = str(data.get("skill_name", "")).strip() content = data.get("content") if not agent_id or not skill_name or not isinstance(content, str): await websocket.send(json.dumps({"type": "error", "message": "update_agent_local_skill requires agent_id, skill_name, and string content."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) try: skills_manager.update_agent_local_skill(config_name=config_name, agent_id=agent_id, skill_name=skill_name, content=content) except (ValueError, FileNotFoundError) as exc: await websocket.send(json.dumps({"type": "error", "message": str(exc)}, ensure_ascii=False)) return await gateway.state_sync.on_system_message(f"Updated local skill {skill_name} for {agent_id}") await gateway._handle_reload_runtime_assets() await websocket.send(json.dumps({"type": "agent_local_skill_updated", "agent_id": agent_id, "skill_name": skill_name}, ensure_ascii=False)) await handle_get_skill_detail(gateway, websocket, {"agent_id": agent_id, "skill_name": skill_name}) async def handle_delete_agent_local_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() skill_name = str(data.get("skill_name", "")).strip() if not agent_id or not skill_name: await websocket.send(json.dumps({"type": "error", "message": "delete_agent_local_skill requires agent_id and skill_name."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) try: skills_manager.delete_agent_local_skill(config_name=config_name, agent_id=agent_id, skill_name=skill_name) skills_manager.forget_agent_skill_overrides(config_name=config_name, agent_id=agent_id, skill_names=[skill_name]) except (ValueError, FileNotFoundError) as exc: await websocket.send(json.dumps({"type": "error", "message": str(exc)}, ensure_ascii=False)) return await gateway.state_sync.on_system_message(f"Deleted local skill {skill_name} for {agent_id}") await gateway._handle_reload_runtime_assets() await websocket.send(json.dumps({"type": "agent_local_skill_deleted", "agent_id": agent_id, "skill_name": skill_name}, ensure_ascii=False)) await handle_get_agent_skills(gateway, websocket, {"agent_id": agent_id}) async def handle_remove_agent_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() skill_name = str(data.get("skill_name", "")).strip() if not agent_id or not skill_name: await websocket.send(json.dumps({"type": "error", "message": "remove_agent_skill requires agent_id and skill_name."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) skill_names = { item.skill_name for item in skills_manager.list_agent_skill_catalog(config_name, agent_id) if item.source != "local" } if skill_name not in skill_names: await websocket.send(json.dumps({"type": "error", "message": f"Unknown shared skill: {skill_name}"}, ensure_ascii=False)) return skills_manager.update_agent_skill_overrides(config_name=config_name, agent_id=agent_id, disable=[skill_name]) await gateway.state_sync.on_system_message(f"Removed shared skill {skill_name} from {agent_id}") await gateway._handle_reload_runtime_assets() await websocket.send(json.dumps({"type": "agent_skill_removed", "agent_id": agent_id, "skill_name": skill_name}, ensure_ascii=False)) await handle_get_agent_skills(gateway, websocket, {"agent_id": agent_id}) async def handle_update_agent_skill(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() skill_name = str(data.get("skill_name", "")).strip() enabled = data.get("enabled") if not agent_id or not skill_name or not isinstance(enabled, bool): await websocket.send(json.dumps({"type": "error", "message": "update_agent_skill requires agent_id, skill_name, and boolean enabled."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) skill_names = {item.skill_name for item in skills_manager.list_agent_skill_catalog(config_name, agent_id)} if skill_name not in skill_names: await websocket.send(json.dumps({"type": "error", "message": f"Unknown skill: {skill_name}"}, ensure_ascii=False)) return if enabled: skills_manager.update_agent_skill_overrides(config_name=config_name, agent_id=agent_id, enable=[skill_name]) await gateway.state_sync.on_system_message(f"Enabled skill {skill_name} for {agent_id}") else: skills_manager.update_agent_skill_overrides(config_name=config_name, agent_id=agent_id, disable=[skill_name]) await gateway.state_sync.on_system_message(f"Disabled skill {skill_name} for {agent_id}") await websocket.send(json.dumps({ "type": "agent_skill_updated", "agent_id": agent_id, "skill_name": skill_name, "enabled": enabled, }, ensure_ascii=False)) await gateway._handle_reload_runtime_assets() await handle_get_agent_skills(gateway, websocket, {"agent_id": agent_id}) async def handle_get_agent_workspace_file(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() filename = gateway._normalize_agent_workspace_filename(data.get("filename")) if not agent_id or not filename: await websocket.send(json.dumps({"type": "error", "message": "get_agent_workspace_file requires agent_id and supported filename."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id) asset_dir.mkdir(parents=True, exist_ok=True) path = asset_dir / filename content = path.read_text(encoding="utf-8") if path.exists() else "" await websocket.send(json.dumps({ "type": "agent_workspace_file_loaded", "config_name": config_name, "agent_id": agent_id, "filename": filename, "content": content, }, ensure_ascii=False)) async def handle_update_agent_workspace_file(gateway: Any, websocket: Any, data: dict[str, Any]) -> None: agent_id = str(data.get("agent_id", "")).strip() filename = gateway._normalize_agent_workspace_filename(data.get("filename")) content = data.get("content") if not agent_id or not filename or not isinstance(content, str): await websocket.send(json.dumps({"type": "error", "message": "update_agent_workspace_file requires agent_id, supported filename, and string content."}, ensure_ascii=False)) return config_name = gateway.config.get("config_name", "default") skills_manager = SkillsManager(project_root=gateway._project_root) asset_dir = skills_manager.get_agent_asset_dir(config_name, agent_id) asset_dir.mkdir(parents=True, exist_ok=True) path = asset_dir / filename path.write_text(content, encoding="utf-8") await gateway.state_sync.on_system_message(f"Updated {filename} for {agent_id}") await websocket.send(json.dumps({"type": "agent_workspace_file_updated", "agent_id": agent_id, "filename": filename}, ensure_ascii=False)) await gateway._handle_reload_runtime_assets() await handle_get_agent_workspace_file(gateway, websocket, {"agent_id": agent_id, "filename": filename})