The review agent was creating too many low-value skills. Three changes tighten the funnel: - review_worker prompt: rewritten in "保守模式". Default decision is do-nothing. To create, all five AND conditions must hold (>=5 tool calls with step dependencies, an explicit fail->fix->success episode, cross-scenario reusability, no >=70% overlap with skill_list, can produce >=300 chars of substance). Adds 7 explicit NO categories (single-tool loops, one-off queries, LLM common knowledge, etc.) and a 4-question self-check before any create. Hard cap of 1 new skill per review, with patch preferred over create. - min_tool_calls: 3 -> 5. Matches the prompt's ">=5 steps" hard condition so trivial turns never reach the LLM reviewer. - Updated config.yaml, the default-value assertion in test_skill_review_worker, and the middleware docstring's "(default 3)" annotation to keep all three references in sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
from deerflow.config.skill_evolution_config import SkillEvolutionConfig
|
|
|
|
|
|
def test_review_worker_config_defaults():
|
|
cfg = SkillEvolutionConfig()
|
|
assert cfg.review_worker.enabled is False
|
|
assert cfg.review_worker.min_tool_calls == 5
|
|
assert cfg.review_worker.timeout_seconds == 120
|
|
assert cfg.review_worker.model_name is None
|
|
assert cfg.review_worker.max_messages == 60
|
|
|
|
|
|
def test_review_worker_config_from_dict():
|
|
cfg = SkillEvolutionConfig.model_validate({
|
|
"enabled": True,
|
|
"review_worker": {"enabled": True, "min_tool_calls": 5, "model_name": "my-model"},
|
|
})
|
|
assert cfg.review_worker.enabled is True
|
|
assert cfg.review_worker.min_tool_calls == 5
|
|
assert cfg.review_worker.model_name == "my-model"
|
|
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
import asyncio
|
|
import pytest
|
|
from langchain_core.messages import HumanMessage
|
|
|
|
|
|
def _make_ai_message(tool_calls_data: list[dict]):
|
|
msg = MagicMock()
|
|
msg.__class__.__name__ = "AIMessage"
|
|
tcs = []
|
|
for tc in tool_calls_data:
|
|
m = MagicMock()
|
|
m.name = tc["name"]
|
|
m.args = tc.get("args", {})
|
|
tcs.append(m)
|
|
msg.tool_calls = tcs
|
|
return msg
|
|
|
|
|
|
def test_extract_tool_call_count_counts_all():
|
|
from deerflow.skills.review_worker import extract_tool_call_count
|
|
msgs = [
|
|
HumanMessage(content="do something"),
|
|
_make_ai_message([{"name": "bash"}, {"name": "read_file"}]),
|
|
_make_ai_message([{"name": "write_file"}]),
|
|
]
|
|
assert extract_tool_call_count(msgs) == 3
|
|
|
|
|
|
def test_extract_tool_call_count_empty():
|
|
from deerflow.skills.review_worker import extract_tool_call_count
|
|
assert extract_tool_call_count([]) == 0
|
|
|
|
|
|
def test_maybe_start_review_disabled_returns_false():
|
|
from deerflow.skills.review_worker import maybe_start_review
|
|
cfg = MagicMock()
|
|
cfg.skill_evolution.enabled = False
|
|
cfg.skill_evolution.review_worker.enabled = True
|
|
cfg.skill_evolution.review_worker.min_tool_calls = 2
|
|
msgs = [_make_ai_message([{"name": "bash"}, {"name": "read_file"}, {"name": "write_file"}])]
|
|
assert maybe_start_review("thread-1", msgs, cfg) is False
|
|
|
|
|
|
def test_maybe_start_review_too_few_tool_calls_returns_false():
|
|
from deerflow.skills.review_worker import maybe_start_review
|
|
cfg = MagicMock()
|
|
cfg.skill_evolution.enabled = True
|
|
cfg.skill_evolution.review_worker.enabled = True
|
|
cfg.skill_evolution.review_worker.min_tool_calls = 5
|
|
msgs = [_make_ai_message([{"name": "bash"}])]
|
|
assert maybe_start_review("thread-4", msgs, cfg) is False
|
|
|
|
|
|
def test_maybe_start_review_starts_task():
|
|
from deerflow.skills import review_worker as rw
|
|
# Reset module state so thread-3 has no lock
|
|
rw._locks.pop("thread-3", None)
|
|
rw._pending.pop("thread-3", None)
|
|
|
|
cfg = MagicMock()
|
|
cfg.skill_evolution.enabled = True
|
|
cfg.skill_evolution.review_worker.enabled = True
|
|
cfg.skill_evolution.review_worker.min_tool_calls = 2
|
|
cfg.skill_evolution.review_worker.timeout_seconds = 30
|
|
cfg.skill_evolution.review_worker.max_messages = 20
|
|
cfg.skill_evolution.review_worker.model_name = None
|
|
|
|
msgs = [_make_ai_message([{"name": "bash"}, {"name": "read_file"}, {"name": "write_file"}])]
|
|
|
|
with patch("deerflow.skills.review_worker.asyncio") as mock_asyncio:
|
|
fake_lock = MagicMock()
|
|
fake_lock.locked.return_value = False
|
|
mock_asyncio.Lock.return_value = fake_lock
|
|
mock_asyncio.create_task = MagicMock()
|
|
|
|
result = rw.maybe_start_review("thread-3", msgs, cfg)
|
|
|
|
assert result is True
|
|
mock_asyncio.create_task.assert_called_once()
|