feat(skills): curator 面板加 pinned 计数 + 运行时输出技能名日志
- get_curator_status 的 agent_created_skills 增加 pinned 计数,与 active/stale/archived 正交;前端技能进化面板同步展示「被钉住」一项 (curator/types.ts、skill-evolution-panel.tsx、zh-CN/en-US/types i18n)。 - run_curator_review 每次运行时把涉及的技能名输出到日志:新增 _log_scanned_skills(扫描到的技能名)与 _candidate_skill_names (候选技能名),logger 与 curator_log 双写。 新增 test_curator_status_counts.py、test_curator_skill_logging.py。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4aee413680
commit
f80a00e211
8 files changed
+179
-2
No files matched your search
@@ -177,6 +177,9 @@ function StatusCard() {
|
||||
<span>
|
||||
{tt.archived}: {status.agent_created_skills.archived}
|
||||
</span>
|
||||
<span>
|
||||
{tt.pinned}: {status.agent_created_skills.pinned}
|
||||
</span>
|
||||
</span>
|
||||
</StatRow>
|
||||
<StatRow label={tt.lastActions} className="sm:col-span-2">
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface CuratorStatus {
|
||||
active: number;
|
||||
stale: number;
|
||||
archived: number;
|
||||
/** 被 pin 的数量;与 active/stale/archived 正交。 */
|
||||
pinned: number;
|
||||
};
|
||||
/** 上次运行 LLM 自述的建议(它"说"要做什么)。 */
|
||||
last_consolidation_suggestions: CuratorSuggestions;
|
||||
|
||||
@@ -546,6 +546,7 @@ export const enUS: Translations = {
|
||||
stale: "Stale",
|
||||
archived: "Archived",
|
||||
total: "Total",
|
||||
pinned: "Pinned",
|
||||
runNow: "Run now",
|
||||
runStarted: "Background run started",
|
||||
runBlockedEvolutionDisabled:
|
||||
|
||||
@@ -474,6 +474,7 @@ export interface Translations {
|
||||
stale: string;
|
||||
archived: string;
|
||||
total: string;
|
||||
pinned: string;
|
||||
runNow: string;
|
||||
runStarted: string;
|
||||
runBlockedEvolutionDisabled: string;
|
||||
|
||||
@@ -522,6 +522,7 @@ export const zhCN: Translations = {
|
||||
stale: "陈旧",
|
||||
archived: "已归档",
|
||||
total: "合计",
|
||||
pinned: "被钉住",
|
||||
runNow: "立即执行",
|
||||
runStarted: "已触发后台执行",
|
||||
runBlockedEvolutionDisabled: "自我进化未开启 —— 请先在下方打开开关。",
|
||||
|
||||
@@ -556,6 +556,41 @@ def _render_candidate_list(user_id: str) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _candidate_skill_names(candidate_list: str) -> list[str]:
|
||||
"""从 :func:`_render_candidate_list` 的文本里抽出技能名。
|
||||
|
||||
候选行形如 ``- {name} [state=..., ...]``,技能名是 ``- `` 之后的第一个 token。
|
||||
"""
|
||||
return [
|
||||
line[2:].split(" ", 1)[0]
|
||||
for line in candidate_list.splitlines()
|
||||
if line.startswith("- ")
|
||||
]
|
||||
|
||||
|
||||
def _log_scanned_skills(user_id: str) -> list[str]:
|
||||
"""记录并返回本次策展扫描到的技能名(Agent 创建、属于该用户)。
|
||||
|
||||
每次技能进化运行时调用 —— 把涉及的技能名打到日志,便于排查"哪些技能
|
||||
被策展程序处理了"。返回排序后的技能名列表。
|
||||
"""
|
||||
from deerflow.skills import curator_log
|
||||
from deerflow.skills.usage import agent_created_entries
|
||||
|
||||
owned = _list_user_owned_skill_names(user_id)
|
||||
scanned = agent_created_entries()
|
||||
if owned is not None:
|
||||
scanned = {n: e for n, e in scanned.items() if n in owned}
|
||||
names = sorted(scanned)
|
||||
logger.info("Curator[%s]: 本次扫描到 %d 个技能 — %s", user_id, len(names), names)
|
||||
curator_log.record(
|
||||
"info",
|
||||
f"本次扫描到 {len(names)} 个 Agent 创建的技能:{', '.join(names) or '(无)'}",
|
||||
user_id=user_id,
|
||||
)
|
||||
return names
|
||||
|
||||
|
||||
_CURATOR_REVIEW_PROMPT = (
|
||||
"You are running as DeerFlow's background skill CURATOR. This is an "
|
||||
"UMBRELLA-BUILDING consolidation pass, not a passive audit and not a "
|
||||
@@ -959,6 +994,9 @@ def run_curator_review(user_id: str) -> dict:
|
||||
logger.info("Curator[%s]: starting run at %s", user_id, now.isoformat())
|
||||
curator_log.record("info", f"本次策展开始,启动时间 {now.isoformat()}", user_id=user_id)
|
||||
|
||||
# 输出本次技能进化涉及的技能名,便于排查。
|
||||
_log_scanned_skills(user_id)
|
||||
|
||||
transition_counters = apply_automatic_transitions(user_id, now)
|
||||
logger.info("Curator[%s]: transitions=%s", user_id, transition_counters)
|
||||
curator_log.record(
|
||||
@@ -972,9 +1010,19 @@ def run_curator_review(user_id: str) -> dict:
|
||||
)
|
||||
|
||||
candidate_list = _render_candidate_list(user_id)
|
||||
candidate_line_count = 0 if not candidate_list.strip() else len([line for line in candidate_list.splitlines() if line.startswith("- ")])
|
||||
candidate_names = _candidate_skill_names(candidate_list)
|
||||
candidate_line_count = len(candidate_names)
|
||||
logger.info(
|
||||
"Curator[%s]: %d 个候选技能进入 LLM 评审 — %s",
|
||||
user_id,
|
||||
candidate_line_count,
|
||||
candidate_names,
|
||||
)
|
||||
curator_log.record(
|
||||
"info", f"进入 LLM 评审的候选技能:共 {candidate_line_count} 个", user_id=user_id
|
||||
"info",
|
||||
f"进入 LLM 评审的候选技能:共 {candidate_line_count} 个"
|
||||
+ (f" —— {', '.join(candidate_names)}" if candidate_names else ""),
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
if candidate_line_count == 0:
|
||||
@@ -1168,9 +1216,13 @@ def get_curator_status(user_id: str) -> dict:
|
||||
if owned is not None:
|
||||
entries = {name: entry for name, entry in entries.items() if name in owned}
|
||||
counts: dict[str, int] = {STATE_ACTIVE: 0, STATE_STALE: 0, STATE_ARCHIVED: 0}
|
||||
pinned_count = 0
|
||||
for entry in entries.values():
|
||||
s = entry.get("state", STATE_ACTIVE)
|
||||
counts[s] = counts.get(s, 0) + 1
|
||||
# pinned 与生命周期状态正交 —— 单独计数。
|
||||
if entry.get("pinned"):
|
||||
pinned_count += 1
|
||||
|
||||
cfg = _get_curator_config(user_id)
|
||||
return {
|
||||
@@ -1184,6 +1236,8 @@ def get_curator_status(user_id: str) -> dict:
|
||||
"active": counts[STATE_ACTIVE],
|
||||
"stale": counts[STATE_STALE],
|
||||
"archived": counts[STATE_ARCHIVED],
|
||||
# 被 pin 的数量;与 active/stale/archived 正交,不计入它们的和。
|
||||
"pinned": pinned_count,
|
||||
},
|
||||
# 上次运行的两份记录:LLM 自述的建议 + 实际发生的工具调用,供前端对照展示。
|
||||
"last_consolidation_suggestions": state.get("last_consolidation_suggestions")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""技能进化运行时把涉及的技能名输出到日志。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from deerflow.skills import curator
|
||||
|
||||
_CURATOR_LOGGER = "deerflow.skills.curator"
|
||||
|
||||
|
||||
class TestCandidateSkillNames:
|
||||
def test_extracts_names_from_candidate_lines(self):
|
||||
text = "- alpha [state=active, use=1]\n- beta-skill [state=stale, use=0]"
|
||||
assert curator._candidate_skill_names(text) == ["alpha", "beta-skill"]
|
||||
|
||||
def test_ignores_non_candidate_lines(self):
|
||||
# _render_candidate_list 无候选时返回的提示文本不应被误识别为技能名
|
||||
assert curator._candidate_skill_names("No agent-created active skills found.") == []
|
||||
|
||||
def test_empty(self):
|
||||
assert curator._candidate_skill_names("") == []
|
||||
|
||||
|
||||
class TestLogScannedSkills:
|
||||
def _patch_common(self, monkeypatch, entries: dict, owned=None) -> None:
|
||||
monkeypatch.setattr(curator, "_list_user_owned_skill_names", lambda uid: owned)
|
||||
monkeypatch.setattr("deerflow.skills.usage.agent_created_entries", lambda: entries)
|
||||
monkeypatch.setattr("deerflow.skills.curator_log.record", lambda *a, **k: None)
|
||||
|
||||
def test_logs_and_returns_sorted_skill_names(self, monkeypatch, caplog):
|
||||
self._patch_common(
|
||||
monkeypatch,
|
||||
{"zeta": {"source": "agent"}, "alpha": {"source": "agent"}},
|
||||
)
|
||||
with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER):
|
||||
names = curator._log_scanned_skills("u1")
|
||||
assert names == ["alpha", "zeta"]
|
||||
assert "alpha" in caplog.text
|
||||
assert "zeta" in caplog.text
|
||||
|
||||
def test_filters_to_owned_skills(self, monkeypatch, caplog):
|
||||
self._patch_common(
|
||||
monkeypatch,
|
||||
{"alpha": {"source": "agent"}, "beta": {"source": "agent"}},
|
||||
owned={"alpha"},
|
||||
)
|
||||
with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER):
|
||||
names = curator._log_scanned_skills("u1")
|
||||
assert names == ["alpha"]
|
||||
|
||||
def test_empty_skills(self, monkeypatch, caplog):
|
||||
self._patch_common(monkeypatch, {})
|
||||
with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER):
|
||||
names = curator._log_scanned_skills("u1")
|
||||
assert names == []
|
||||
@@ -0,0 +1,59 @@
|
||||
"""get_curator_status 的技能计数 —— 含与生命周期正交的 pinned 计数。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from deerflow.config.skill_evolution_config import CuratorConfig
|
||||
from deerflow.skills import curator
|
||||
|
||||
|
||||
def _patch(monkeypatch, entries: dict) -> None:
|
||||
"""注入受控依赖:固定 curator 状态/配置,owned 用 legacy 全量,entries 由测试给定。"""
|
||||
monkeypatch.setattr(curator, "_read_curator_state", lambda uid: {})
|
||||
monkeypatch.setattr(curator, "_list_user_owned_skill_names", lambda uid: None)
|
||||
monkeypatch.setattr(curator, "_get_curator_config", lambda uid: CuratorConfig(enabled=True))
|
||||
monkeypatch.setattr(
|
||||
"deerflow.skills.usage.agent_created_entries",
|
||||
lambda: entries,
|
||||
)
|
||||
|
||||
|
||||
def test_counts_by_state(monkeypatch):
|
||||
entries = {
|
||||
"a": {"source": "agent", "state": "active"},
|
||||
"b": {"source": "agent", "state": "active"},
|
||||
"c": {"source": "agent", "state": "stale"},
|
||||
"d": {"source": "agent", "state": "archived"},
|
||||
}
|
||||
_patch(monkeypatch, entries)
|
||||
counts = curator.get_curator_status("u1")["agent_created_skills"]
|
||||
assert counts == {"total": 4, "active": 2, "stale": 1, "archived": 1, "pinned": 0}
|
||||
|
||||
|
||||
def test_pinned_count_is_orthogonal_to_state(monkeypatch):
|
||||
# pinned 与 active/stale/archived 正交:一个 active+pinned、一个 archived+pinned
|
||||
entries = {
|
||||
"a": {"source": "agent", "state": "active", "pinned": True},
|
||||
"b": {"source": "agent", "state": "active"},
|
||||
"c": {"source": "agent", "state": "archived", "pinned": True},
|
||||
"d": {"source": "agent", "state": "stale"},
|
||||
}
|
||||
_patch(monkeypatch, entries)
|
||||
counts = curator.get_curator_status("u1")["agent_created_skills"]
|
||||
assert counts["total"] == 4
|
||||
assert counts["active"] == 2
|
||||
assert counts["stale"] == 1
|
||||
assert counts["archived"] == 1
|
||||
# pinned 单独统计,不受状态影响,也不计入上面三者之和
|
||||
assert counts["pinned"] == 2
|
||||
|
||||
|
||||
def test_pinned_zero_when_none_pinned(monkeypatch):
|
||||
_patch(monkeypatch, {"a": {"source": "agent", "state": "active"}})
|
||||
counts = curator.get_curator_status("u1")["agent_created_skills"]
|
||||
assert counts["pinned"] == 0
|
||||
|
||||
|
||||
def test_empty_skills(monkeypatch):
|
||||
_patch(monkeypatch, {})
|
||||
counts = curator.get_curator_status("u1")["agent_created_skills"]
|
||||
assert counts == {"total": 0, "active": 0, "stale": 0, "archived": 0, "pinned": 0}
|
||||
Reference in new issue
Block a user