mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Load skills into chat history
Switch the Skills catalog UI/API from scope-wide prompt pins to current-chat history loading, and remove user-facing removal of loaded skills. Disable legacy active-skill prompt injection so forgotten scope config no longer inflates new-chat prompts.
This commit is contained in:
parent
5ac4f75a47
commit
dea64ddad0
12 changed files with 293 additions and 217 deletions
163
tests/test_skills_catalog_api.py
Normal file
163
tests/test_skills_catalog_api.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from plugins._skills.api import skills_catalog
|
||||
|
||||
|
||||
class FakeHistory:
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
|
||||
def output(self):
|
||||
return self.messages
|
||||
|
||||
|
||||
class FakeContext:
|
||||
def __init__(self):
|
||||
self.id = "ctx"
|
||||
self.data = {}
|
||||
self.agent = FakeAgent(self)
|
||||
|
||||
def get_agent(self):
|
||||
return self.agent
|
||||
|
||||
def get_data(self, key, recursive=True):
|
||||
return self.data.get(key)
|
||||
|
||||
def set_data(self, key, value, recursive=True):
|
||||
self.data[key] = value
|
||||
|
||||
|
||||
class FakeAgent:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.history = FakeHistory()
|
||||
self.tool_results = []
|
||||
|
||||
def hist_add_tool_result(self, tool_name, tool_result, **kwargs):
|
||||
content = {"tool_name": tool_name, "tool_result": tool_result, **kwargs}
|
||||
message = {"ai": False, "content": content}
|
||||
self.tool_results.append(content)
|
||||
self.history.messages.append(message)
|
||||
return SimpleNamespace(output=lambda: [message])
|
||||
|
||||
|
||||
def _patch_catalog(monkeypatch, context):
|
||||
skill = {
|
||||
"name": "demo-skill",
|
||||
"description": "Demo skill.",
|
||||
"path": "/a0/skills/demo-skill",
|
||||
"origin": "Built-in",
|
||||
"hidden": False,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
skills_catalog.AgentContext,
|
||||
"get",
|
||||
staticmethod(lambda context_id: context if context_id == context.id else None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
skills_catalog.projects,
|
||||
"get_context_project_name",
|
||||
lambda _context: "",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
skills_catalog.skills,
|
||||
"list_skill_catalog",
|
||||
lambda *args, **kwargs: [skill],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
skills_catalog.skills,
|
||||
"load_skill_for_agent",
|
||||
lambda skill_name, agent: f"Skill: {skill_name}\n\nInstructions:\nUse it.",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
skills_catalog.skills,
|
||||
"add_loaded_skill_name",
|
||||
lambda agent, skill_name: agent.context.set_data("loaded_skills", [skill_name]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
skills_catalog.skills,
|
||||
"get_loaded_skill_entries",
|
||||
lambda agent: [
|
||||
{"name": name} for name in (agent.context.get_data("loaded_skills") or [])
|
||||
] if agent else [],
|
||||
)
|
||||
monkeypatch.setattr(skills_catalog.skills, "get_scope_active_skills", lambda agent: [])
|
||||
monkeypatch.setattr(skills_catalog.skills, "get_scope_hidden_skills", lambda agent: [])
|
||||
monkeypatch.setattr(skills_catalog.skills, "get_chat_active_skills", lambda context: [])
|
||||
monkeypatch.setattr(skills_catalog.skills, "get_chat_disabled_skills", lambda context: [])
|
||||
monkeypatch.setattr(skills_catalog.skills, "get_chat_visible_skills", lambda context: [])
|
||||
monkeypatch.setattr(skills_catalog.skills, "get_hidden_skills", lambda agent: [])
|
||||
monkeypatch.setattr(skills_catalog.skills, "get_max_active_skills", lambda **kwargs: 20)
|
||||
|
||||
saved = []
|
||||
monkeypatch.setattr(skills_catalog, "save_tmp_chat", lambda ctx: saved.append(ctx.id))
|
||||
return saved
|
||||
|
||||
|
||||
def test_skills_catalog_activate_loads_skill_into_chat_history(monkeypatch):
|
||||
context = FakeContext()
|
||||
saved = _patch_catalog(monkeypatch, context)
|
||||
handler = skills_catalog.SkillsCatalog(None, None)
|
||||
|
||||
response = asyncio.run(
|
||||
handler.process(
|
||||
{
|
||||
"action": "activate",
|
||||
"context_id": "ctx",
|
||||
"skill": {"name": "demo-skill", "path": "/a0/skills/demo-skill"},
|
||||
},
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
assert response["ok"] is True, response
|
||||
assert context.get_data("loaded_skills") == ["demo-skill"]
|
||||
assert len(context.agent.tool_results) == 1
|
||||
assert "Skill: demo-skill" in context.agent.tool_results[0]["tool_result"]
|
||||
assert response["active_skills"][0]["state_source"] == "Loaded in chat history"
|
||||
assert saved == ["ctx"]
|
||||
|
||||
duplicate = asyncio.run(
|
||||
handler.process(
|
||||
{
|
||||
"action": "activate",
|
||||
"context_id": "ctx",
|
||||
"skill": {"name": "demo-skill", "path": "/a0/skills/demo-skill"},
|
||||
},
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
assert duplicate["ok"] is True
|
||||
assert len(context.agent.tool_results) == 1
|
||||
|
||||
|
||||
def test_skills_catalog_deactivate_does_not_remove_loaded_skill(monkeypatch):
|
||||
context = FakeContext()
|
||||
_patch_catalog(monkeypatch, context)
|
||||
context.set_data("loaded_skills", ["demo-skill"])
|
||||
handler = skills_catalog.SkillsCatalog(None, None)
|
||||
|
||||
response = asyncio.run(
|
||||
handler.process(
|
||||
{
|
||||
"action": "deactivate",
|
||||
"context_id": "ctx",
|
||||
"skill": {"name": "demo-skill"},
|
||||
},
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
assert response["ok"] is False
|
||||
assert "cannot be removed" in response["error"]
|
||||
assert context.get_data("loaded_skills") == ["demo-skill"]
|
||||
assert context.agent.tool_results == []
|
||||
|
|
@ -150,6 +150,35 @@ def test_hidden_skills_are_not_capped_like_active_skills():
|
|||
assert len(runtime.get_hidden_skills(agent)) == 25
|
||||
|
||||
|
||||
def test_active_skill_prompt_protocol_is_disabled(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
runtime.plugin_helpers,
|
||||
"get_plugin_config",
|
||||
lambda *args, **kwargs: _scope_config([{"name": "Pinned"}]),
|
||||
)
|
||||
agent = DummyAgent()
|
||||
|
||||
assert runtime.get_active_skills(agent) == [{"name": "Pinned"}]
|
||||
assert runtime.build_active_skills_prompt(agent) == ""
|
||||
|
||||
|
||||
def test_hiding_skill_does_not_unload_history_loaded_skill():
|
||||
agent = DummyAgent()
|
||||
agent.context.set_data(
|
||||
runtime.CONTEXT_DATA_NAME_LOADED_SKILLS,
|
||||
["history-skill"],
|
||||
)
|
||||
|
||||
runtime.hide_chat_skill(agent, {"name": "history-skill"})
|
||||
|
||||
assert agent.context.get_data(runtime.CONTEXT_DATA_NAME_LOADED_SKILLS) == [
|
||||
"history-skill"
|
||||
]
|
||||
assert runtime.get_chat_disabled_skills(agent.context) == [
|
||||
{"name": "history-skill"}
|
||||
]
|
||||
|
||||
|
||||
def test_chat_activation_can_override_scope_defaults(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
runtime.plugin_helpers,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue