mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Simplify relevant skill recall
Search the raw user message when recalling relevant skills instead of the rendered history wrapper. Replace the stopword catalog with structural matching: names use normal terms, tags/triggers use long terms or phrase matches, and descriptions require phrase matches.
This commit is contained in:
parent
dea64ddad0
commit
0de0fcec0e
6 changed files with 90 additions and 9 deletions
|
|
@ -16,6 +16,7 @@
|
|||
- Keep injected content bounded and clearly attributed.
|
||||
- Preserve ordering where later prompt extras depend on earlier recall or load results.
|
||||
- Do not expose secrets or private files from workdir extras.
|
||||
- Relevant-skill recall should search the raw user message when available, not the rendered history wrapper.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,13 @@ class RecallRelevantSkills(Extension):
|
|||
if not self.agent or loop_data.iteration != 0:
|
||||
return
|
||||
|
||||
user_instruction = (
|
||||
loop_data.user_message.output_text() if loop_data.user_message else ""
|
||||
).strip()
|
||||
content = loop_data.user_message.content if loop_data.user_message else ""
|
||||
if isinstance(content, dict):
|
||||
user_instruction = str(content.get("user_message") or "").strip()
|
||||
else:
|
||||
user_instruction = (
|
||||
loop_data.user_message.output_text() if loop_data.user_message else ""
|
||||
).strip()
|
||||
if len(user_instruction) < 8:
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -543,11 +543,15 @@ def search_skills(
|
|||
if not q:
|
||||
return []
|
||||
|
||||
raw_terms = [t for t in re.split(r"\s+", q) if t]
|
||||
raw_terms = re.findall(r"[a-z0-9][a-z0-9_-]*", q)
|
||||
terms = [
|
||||
t for t in raw_terms
|
||||
if len(t) >= 3 or any(ch.isdigit() for ch in t)
|
||||
] or raw_terms
|
||||
if len(t) >= 4 or any(ch.isdigit() for ch in t)
|
||||
]
|
||||
long_terms = [
|
||||
t for t in raw_terms
|
||||
if len(t) >= 6 or any(ch.isdigit() for ch in t)
|
||||
]
|
||||
candidates = list_skills(agent, include_hidden=include_hidden)
|
||||
|
||||
scored: List[Tuple[int, Skill]] = []
|
||||
|
|
@ -568,14 +572,13 @@ def search_skills(
|
|||
score += 4
|
||||
if any(q in tag for tag in tags):
|
||||
score += 3
|
||||
if any(q in trigger for trigger in triggers):
|
||||
if any(q in trigger or trigger in q for trigger in triggers):
|
||||
score += 8
|
||||
|
||||
for term in terms:
|
||||
if term in name:
|
||||
score += 3
|
||||
if term in desc:
|
||||
score += 2
|
||||
for term in long_terms:
|
||||
if any(term in tag for tag in tags):
|
||||
score += 1
|
||||
if any(term in trigger for trigger in triggers):
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
- Loaded skill names are chat-wide context data under `CONTEXT_DATA_NAME_LOADED_SKILLS`; legacy agent-local `loaded_skills` lists are migrated into context data and cleared when read.
|
||||
- Loaded skill bodies live in chat history; hiding a skill changes catalog visibility but does not remove the loaded-skill ledger.
|
||||
- `build_active_skills_prompt()` returns empty because selected skills are loaded through history, not prompt protocol.
|
||||
- `search_skills()` normalizes query words, scores normal terms against skill names, and scores only long terms against tags/triggers; descriptions match only full query phrases so generic prose does not produce irrelevant suggestions.
|
||||
- Invalid `SKILL.md` frontmatter emits a once-per-path scan warning with the skipped skill path/name and a line number when the parser can identify one directly.
|
||||
- Observed side-effect areas: filesystem reads, filesystem deletion, plugin state, settings/state persistence, context data, secret handling.
|
||||
- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `os`, `pathlib`, `re`, `typing`.
|
||||
|
|
|
|||
|
|
@ -473,6 +473,27 @@ def test_browser_skills_rank_for_browser_trigger_phrases(monkeypatch):
|
|||
assert "browser-form-workflows" in form_results
|
||||
|
||||
|
||||
def test_skill_search_does_not_score_description_terms_alone(monkeypatch):
|
||||
browser_automation = runtime.Skill(
|
||||
name="browser-automation",
|
||||
description=(
|
||||
"Use for browser automation, screenshots, forms, uploads, "
|
||||
"and complex tool workflows."
|
||||
),
|
||||
path=Path("/skills/browser-automation"),
|
||||
skill_md_path=Path("/skills/browser-automation/SKILL.md"),
|
||||
)
|
||||
monkeypatch.setattr(runtime, "list_skills", lambda *args, **kwargs: [browser_automation])
|
||||
|
||||
rendered_user_message = (
|
||||
'user: {"user_message": "Please reply with exactly OK. Do not use tools.", '
|
||||
'"attachments": []}'
|
||||
)
|
||||
|
||||
assert runtime.search_skills(rendered_user_message) == []
|
||||
assert runtime.search_skills("Open a browser screenshot?", limit=1) == [browser_automation]
|
||||
|
||||
|
||||
def test_host_computer_use_ranks_before_linux_desktop_for_host_screen_queries(monkeypatch):
|
||||
host_skill = runtime.skill_from_markdown(
|
||||
PROJECT_ROOT / "plugins" / "_a0_connector" / "skills" / "host-computer-use" / "SKILL.md"
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ def _load_skills_tool(monkeypatch, skill_root: Path):
|
|||
skills_stub.set_loaded_skill_names = _set_loaded_skill_names
|
||||
skills_stub.skill_instruction_name = _skill_instruction_name
|
||||
monkeypatch.setitem(sys.modules, "helpers.skills", skills_stub)
|
||||
import helpers
|
||||
monkeypatch.setattr(helpers, "skills", skills_stub, raising=False)
|
||||
|
||||
print_style_stub = types.ModuleType("helpers.print_style")
|
||||
print_style_stub.PrintStyle = lambda *args, **kwargs: types.SimpleNamespace(
|
||||
|
|
@ -213,6 +215,33 @@ def _load_loaded_skills_extension(monkeypatch, skill_root: Path):
|
|||
return importlib.import_module(module_name)
|
||||
|
||||
|
||||
def _load_relevant_skills_extension(monkeypatch, queries: list[str]):
|
||||
extension_stub = types.ModuleType("helpers.extension")
|
||||
extension_stub.Extension = _FakeExtension
|
||||
monkeypatch.setitem(sys.modules, "helpers.extension", extension_stub)
|
||||
|
||||
agent_stub = types.ModuleType("agent")
|
||||
agent_stub.LoopData = lambda **kwargs: types.SimpleNamespace(**kwargs)
|
||||
monkeypatch.setitem(sys.modules, "agent", agent_stub)
|
||||
|
||||
skills_stub = types.ModuleType("helpers.skills")
|
||||
|
||||
def _search_skills(query, *args, **kwargs):
|
||||
queries.append(query)
|
||||
return []
|
||||
|
||||
skills_stub.search_skills = _search_skills
|
||||
monkeypatch.setitem(sys.modules, "helpers.skills", skills_stub)
|
||||
|
||||
import helpers
|
||||
|
||||
monkeypatch.setattr(helpers, "skills", skills_stub, raising=False)
|
||||
|
||||
module_name = "extensions.python.message_loop_prompts_after._63_recall_relevant_skills"
|
||||
sys.modules.pop(module_name, None)
|
||||
return importlib.import_module(module_name)
|
||||
|
||||
|
||||
def _load_computer_use_remote_tool(monkeypatch):
|
||||
_install_tool_stub(monkeypatch)
|
||||
|
||||
|
|
@ -471,6 +500,28 @@ def test_loaded_skills_extension_keeps_reattachments_under_budget(
|
|||
assert agent.added_tool_results == []
|
||||
|
||||
|
||||
def test_relevant_skill_recall_uses_raw_user_message(monkeypatch):
|
||||
queries: list[str] = []
|
||||
module = _load_relevant_skills_extension(monkeypatch, queries)
|
||||
agent = types.SimpleNamespace()
|
||||
loop_data = types.SimpleNamespace(
|
||||
iteration=0,
|
||||
user_message=types.SimpleNamespace(
|
||||
content={
|
||||
"system_message": [],
|
||||
"user_message": "Open a browser and take a screenshot.",
|
||||
"attachments": [],
|
||||
},
|
||||
output_text=lambda: 'user: {"user_message": "wrapped"}',
|
||||
),
|
||||
extras_temporary={},
|
||||
)
|
||||
|
||||
asyncio.run(module.RecallRelevantSkills(agent).execute(loop_data=loop_data))
|
||||
|
||||
assert queries == ["Open a browser and take a screenshot."]
|
||||
|
||||
|
||||
def test_skills_tool_read_file_action_reads_inside_skill_dir(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue