Add per-agent skill workspaces and TraderView management

This commit is contained in:
2026-03-17 13:55:14 +08:00
parent 1f5ee3698e
commit 2daf5717ba
35 changed files with 4774 additions and 331 deletions

View File

@@ -382,3 +382,341 @@ async def test_refresh_market_store_for_watchlist_emits_system_messages(monkeypa
assert gateway.state_sync.system_messages[0] == "正在同步自选股市场数据: AAPL, MSFT"
assert "自选股市场数据已同步:" in gateway.state_sync.system_messages[1]
assert "AAPL prices=3 news=4" in gateway.state_sync.system_messages[1]
@pytest.mark.asyncio
async def test_handle_get_agent_skills_returns_statuses(tmp_path):
builtin_root = tmp_path / "backend" / "skills" / "builtin"
for name in ("risk_review", "extra_guard"):
skill_dir = builtin_root / name
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
f"---\nname: {name}\ndescription: {name} desc\n---\n",
encoding="utf-8",
)
agent_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
agent_dir.mkdir(parents=True, exist_ok=True)
(agent_dir / "agent.yaml").write_text(
"enabled_skills:\n"
" - extra_guard\n"
"disabled_skills:\n"
" - risk_review\n",
encoding="utf-8",
)
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
await gateway._handle_get_agent_skills(
websocket,
{"agent_id": "risk_manager"},
)
assert websocket.messages[-1]["type"] == "agent_skills_loaded"
statuses = {
row["skill_name"]: row["status"]
for row in websocket.messages[-1]["skills"]
}
assert statuses["extra_guard"] == "enabled"
assert statuses["risk_review"] == "disabled"
@pytest.mark.asyncio
async def test_handle_get_agent_profile_returns_model_and_tool_groups(monkeypatch, tmp_path):
agent_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
agent_dir.mkdir(parents=True, exist_ok=True)
(agent_dir / "agent.yaml").write_text(
"prompt_files:\n"
" - SOUL.md\n"
" - MEMORY.md\n"
"active_tool_groups:\n"
" - risk_ops\n"
"disabled_tool_groups:\n"
" - legacy_group\n",
encoding="utf-8",
)
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
monkeypatch.setattr(
gateway_module,
"load_agent_profiles",
lambda: {"risk_manager": {"skills": ["risk_review"], "active_tool_groups": ["risk_ops", "legacy_group"]}},
)
monkeypatch.setattr(
gateway_module,
"get_agent_model_info",
lambda agent_id: ("gpt-4o-mini", "OPENAI"),
)
class _Bootstrap:
@staticmethod
def agent_override(_agent_id):
return {}
monkeypatch.setattr(
gateway_module,
"get_bootstrap_config_for_run",
lambda project_root, config_name: _Bootstrap(),
)
await gateway._handle_get_agent_profile(
websocket,
{"agent_id": "risk_manager"},
)
assert websocket.messages[-1]["type"] == "agent_profile_loaded"
profile = websocket.messages[-1]["profile"]
assert profile["model_name"] == "gpt-4o-mini"
assert profile["model_provider"] == "OPENAI"
assert profile["prompt_files"] == ["SOUL.md", "MEMORY.md"]
assert profile["active_tool_groups"] == ["risk_ops"]
assert profile["disabled_tool_groups"] == ["legacy_group"]
@pytest.mark.asyncio
async def test_handle_get_skill_detail_returns_markdown_body(tmp_path):
skill_dir = tmp_path / "backend" / "skills" / "builtin" / "risk_review"
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
"---\nname: 风险审查\ndescription: 说明\nversion: 1.0.0\n---\n# 风险审查\n\n完整正文\n",
encoding="utf-8",
)
gateway = make_gateway()
gateway._project_root = tmp_path
websocket = DummyWebSocket()
await gateway._handle_get_skill_detail(
websocket,
{"skill_name": "risk_review"},
)
assert websocket.messages[-1]["type"] == "skill_detail_loaded"
assert websocket.messages[-1]["skill"]["name"] == "风险审查"
assert websocket.messages[-1]["skill"]["version"] == "1.0.0"
assert websocket.messages[-1]["skill"]["content"] == "# 风险审查\n\n完整正文"
@pytest.mark.asyncio
async def test_handle_get_skill_detail_prefers_agent_local_skill(tmp_path):
skill_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "skills" / "local" / "local_guard"
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
"---\nname: 本地风控\ndescription: 本地说明\nversion: 1.0.0\n---\n# 本地风控\n\n本地正文\n",
encoding="utf-8",
)
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
await gateway._handle_get_skill_detail(
websocket,
{"agent_id": "risk_manager", "skill_name": "local_guard"},
)
assert websocket.messages[-1]["type"] == "skill_detail_loaded"
assert websocket.messages[-1]["agent_id"] == "risk_manager"
assert websocket.messages[-1]["skill"]["source"] == "local"
assert websocket.messages[-1]["skill"]["content"] == "# 本地风控\n\n本地正文"
@pytest.mark.asyncio
async def test_handle_update_agent_skill_persists_and_returns_refresh(monkeypatch, tmp_path):
skill_dir = tmp_path / "backend" / "skills" / "builtin" / "extra_guard"
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
"---\nname: extra_guard\ndescription: desc\n---\n",
encoding="utf-8",
)
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
async def _noop_reload():
return None
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
await gateway._handle_update_agent_skill(
websocket,
{
"agent_id": "risk_manager",
"skill_name": "extra_guard",
"enabled": True,
},
)
assert websocket.messages[0]["type"] == "agent_skill_updated"
assert websocket.messages[-1]["type"] == "agent_skills_loaded"
agent_yaml = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "agent.yaml"
assert "extra_guard" in agent_yaml.read_text(encoding="utf-8")
@pytest.mark.asyncio
async def test_handle_create_and_update_agent_local_skill(monkeypatch, tmp_path):
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
async def _noop_reload():
return None
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
await gateway._handle_create_agent_local_skill(
websocket,
{"agent_id": "risk_manager", "skill_name": "local_guard"},
)
assert websocket.messages[0]["type"] == "agent_local_skill_created"
assert websocket.messages[1]["type"] == "agent_skills_loaded"
assert websocket.messages[2]["type"] == "skill_detail_loaded"
target = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "skills" / "local" / "local_guard" / "SKILL.md"
assert target.exists()
websocket.messages.clear()
await gateway._handle_update_agent_local_skill(
websocket,
{
"agent_id": "risk_manager",
"skill_name": "local_guard",
"content": "---\nname: 本地风控\ndescription: 更新后\nversion: 1.0.0\n---\n# 本地风控\n\n更新正文\n",
},
)
assert websocket.messages[0]["type"] == "agent_local_skill_updated"
assert websocket.messages[1]["type"] == "skill_detail_loaded"
assert "更新正文" in target.read_text(encoding="utf-8")
@pytest.mark.asyncio
async def test_handle_delete_agent_local_skill(monkeypatch, tmp_path):
skill_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "skills" / "local" / "local_guard"
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
"---\nname: 本地风控\ndescription: desc\nversion: 1.0.0\n---\n",
encoding="utf-8",
)
agent_yaml = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "agent.yaml"
agent_yaml.parent.mkdir(parents=True, exist_ok=True)
agent_yaml.write_text(
"enabled_skills:\n"
" - local_guard\n"
"disabled_skills:\n"
" - local_guard\n",
encoding="utf-8",
)
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
async def _noop_reload():
return None
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
await gateway._handle_delete_agent_local_skill(
websocket,
{"agent_id": "risk_manager", "skill_name": "local_guard"},
)
assert websocket.messages[0]["type"] == "agent_local_skill_deleted"
assert websocket.messages[1]["type"] == "agent_skills_loaded"
assert not skill_dir.exists()
assert "local_guard" not in agent_yaml.read_text(encoding="utf-8")
@pytest.mark.asyncio
async def test_handle_remove_agent_skill_marks_disabled(monkeypatch, tmp_path):
skill_dir = tmp_path / "backend" / "skills" / "builtin" / "risk_review"
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
"---\nname: 风险审查\ndescription: desc\nversion: 1.0.0\n---\n",
encoding="utf-8",
)
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
async def _noop_reload():
return None
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
await gateway._handle_remove_agent_skill(
websocket,
{"agent_id": "risk_manager", "skill_name": "risk_review"},
)
assert websocket.messages[0]["type"] == "agent_skill_removed"
assert websocket.messages[1]["type"] == "agent_skills_loaded"
agent_yaml = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "agent.yaml"
assert "risk_review" in agent_yaml.read_text(encoding="utf-8")
@pytest.mark.asyncio
async def test_handle_get_agent_workspace_file_returns_content(tmp_path):
file_path = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "SOUL.md"
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("soul content", encoding="utf-8")
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
await gateway._handle_get_agent_workspace_file(
websocket,
{"agent_id": "risk_manager", "filename": "SOUL.md"},
)
assert websocket.messages[-1] == {
"type": "agent_workspace_file_loaded",
"config_name": "demo",
"agent_id": "risk_manager",
"filename": "SOUL.md",
"content": "soul content",
}
@pytest.mark.asyncio
async def test_handle_update_agent_workspace_file_persists_and_returns_refresh(monkeypatch, tmp_path):
gateway = make_gateway()
gateway.config["config_name"] = "demo"
gateway._project_root = tmp_path
websocket = DummyWebSocket()
async def _noop_reload():
return None
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
await gateway._handle_update_agent_workspace_file(
websocket,
{
"agent_id": "risk_manager",
"filename": "SOUL.md",
"content": "updated soul",
},
)
assert websocket.messages[0]["type"] == "agent_workspace_file_updated"
assert websocket.messages[-1]["type"] == "agent_workspace_file_loaded"
target = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "SOUL.md"
assert target.read_text(encoding="utf-8") == "updated soul"