feat: add skill filter + group system prompt plumbing
This commit is contained in:
@@ -165,6 +165,33 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies skill filters, including empty lists", async () => {
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "alpha"),
|
||||||
|
name: "alpha",
|
||||||
|
description: "Alpha skill",
|
||||||
|
});
|
||||||
|
await writeSkill({
|
||||||
|
dir: path.join(workspaceDir, "skills", "beta"),
|
||||||
|
name: "beta",
|
||||||
|
description: "Beta skill",
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredPrompt = buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||||
|
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||||
|
skillFilter: ["alpha"],
|
||||||
|
});
|
||||||
|
expect(filteredPrompt).toContain("alpha");
|
||||||
|
expect(filteredPrompt).not.toContain("beta");
|
||||||
|
|
||||||
|
const emptyPrompt = buildWorkspaceSkillsPrompt(workspaceDir, {
|
||||||
|
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||||
|
skillFilter: [],
|
||||||
|
});
|
||||||
|
expect(emptyPrompt).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
it("prefers workspace skills over managed skills", async () => {
|
it("prefers workspace skills over managed skills", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-"));
|
||||||
const managedDir = path.join(workspaceDir, ".managed");
|
const managedDir = path.join(workspaceDir, ".managed");
|
||||||
|
|||||||
@@ -382,8 +382,27 @@ function shouldIncludeSkill(params: {
|
|||||||
function filterSkillEntries(
|
function filterSkillEntries(
|
||||||
entries: SkillEntry[],
|
entries: SkillEntry[],
|
||||||
config?: ClawdbotConfig,
|
config?: ClawdbotConfig,
|
||||||
|
skillFilter?: string[],
|
||||||
): SkillEntry[] {
|
): SkillEntry[] {
|
||||||
return entries.filter((entry) => shouldIncludeSkill({ entry, config }));
|
let filtered = entries.filter((entry) =>
|
||||||
|
shouldIncludeSkill({ entry, config }),
|
||||||
|
);
|
||||||
|
// If skillFilter is provided, only include skills in the filter list.
|
||||||
|
if (skillFilter !== undefined) {
|
||||||
|
const normalized = skillFilter
|
||||||
|
.map((entry) => String(entry).trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const label = normalized.length > 0 ? normalized.join(", ") : "(none)";
|
||||||
|
console.log(`[skills] Applying skill filter: ${label}`);
|
||||||
|
filtered =
|
||||||
|
normalized.length > 0
|
||||||
|
? filtered.filter((entry) => normalized.includes(entry.skill.name))
|
||||||
|
: [];
|
||||||
|
console.log(
|
||||||
|
`[skills] After filter: ${filtered.map((entry) => entry.skill.name).join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applySkillEnvOverrides(params: {
|
export function applySkillEnvOverrides(params: {
|
||||||
@@ -548,10 +567,16 @@ export function buildWorkspaceSkillSnapshot(
|
|||||||
managedSkillsDir?: string;
|
managedSkillsDir?: string;
|
||||||
bundledSkillsDir?: string;
|
bundledSkillsDir?: string;
|
||||||
entries?: SkillEntry[];
|
entries?: SkillEntry[];
|
||||||
|
/** If provided, only include skills with these names */
|
||||||
|
skillFilter?: string[];
|
||||||
},
|
},
|
||||||
): SkillSnapshot {
|
): SkillSnapshot {
|
||||||
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
|
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
|
||||||
const eligible = filterSkillEntries(skillEntries, opts?.config);
|
const eligible = filterSkillEntries(
|
||||||
|
skillEntries,
|
||||||
|
opts?.config,
|
||||||
|
opts?.skillFilter,
|
||||||
|
);
|
||||||
const resolvedSkills = eligible.map((entry) => entry.skill);
|
const resolvedSkills = eligible.map((entry) => entry.skill);
|
||||||
return {
|
return {
|
||||||
prompt: formatSkillsForPrompt(resolvedSkills),
|
prompt: formatSkillsForPrompt(resolvedSkills),
|
||||||
@@ -570,10 +595,16 @@ export function buildWorkspaceSkillsPrompt(
|
|||||||
managedSkillsDir?: string;
|
managedSkillsDir?: string;
|
||||||
bundledSkillsDir?: string;
|
bundledSkillsDir?: string;
|
||||||
entries?: SkillEntry[];
|
entries?: SkillEntry[];
|
||||||
|
/** If provided, only include skills with these names */
|
||||||
|
skillFilter?: string[];
|
||||||
},
|
},
|
||||||
): string {
|
): string {
|
||||||
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
|
const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts);
|
||||||
const eligible = filterSkillEntries(skillEntries, opts?.config);
|
const eligible = filterSkillEntries(
|
||||||
|
skillEntries,
|
||||||
|
opts?.config,
|
||||||
|
opts?.skillFilter,
|
||||||
|
);
|
||||||
return formatSkillsForPrompt(eligible.map((entry) => entry.skill));
|
return formatSkillsForPrompt(eligible.map((entry) => entry.skill));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -593,6 +593,10 @@ export async function getReplyFromConfig(
|
|||||||
silentToken: SILENT_REPLY_TOKEN,
|
silentToken: SILENT_REPLY_TOKEN,
|
||||||
})
|
})
|
||||||
: "";
|
: "";
|
||||||
|
const groupSystemPrompt = sessionCtx.GroupSystemPrompt?.trim() ?? "";
|
||||||
|
const extraSystemPrompt = [groupIntro, groupSystemPrompt]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n");
|
||||||
const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
|
const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
|
||||||
const rawBodyTrimmed = (ctx.Body ?? "").trim();
|
const rawBodyTrimmed = (ctx.Body ?? "").trim();
|
||||||
const baseBodyTrimmedRaw = baseBody.trim();
|
const baseBodyTrimmedRaw = baseBody.trim();
|
||||||
@@ -651,6 +655,7 @@ export async function getReplyFromConfig(
|
|||||||
isFirstTurnInSession,
|
isFirstTurnInSession,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
cfg,
|
cfg,
|
||||||
|
skillFilter: opts?.skillFilter,
|
||||||
});
|
});
|
||||||
sessionEntry = skillResult.sessionEntry ?? sessionEntry;
|
sessionEntry = skillResult.sessionEntry ?? sessionEntry;
|
||||||
systemSent = skillResult.systemSent;
|
systemSent = skillResult.systemSent;
|
||||||
@@ -759,7 +764,7 @@ export async function getReplyFromConfig(
|
|||||||
blockReplyBreak: resolvedBlockStreamingBreak,
|
blockReplyBreak: resolvedBlockStreamingBreak,
|
||||||
ownerNumbers:
|
ownerNumbers:
|
||||||
command.ownerList.length > 0 ? command.ownerList : undefined,
|
command.ownerList.length > 0 ? command.ownerList : undefined,
|
||||||
extraSystemPrompt: groupIntro || undefined,
|
extraSystemPrompt: extraSystemPrompt || undefined,
|
||||||
...(provider === "ollama" ? { enforceFinalTag: true } : {}),
|
...(provider === "ollama" ? { enforceFinalTag: true } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export async function ensureSkillSnapshot(params: {
|
|||||||
isFirstTurnInSession: boolean;
|
isFirstTurnInSession: boolean;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
|
/** If provided, only load skills with these names (for per-channel skill filtering) */
|
||||||
|
skillFilter?: string[];
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
sessionEntry?: SessionEntry;
|
sessionEntry?: SessionEntry;
|
||||||
skillsSnapshot?: SessionEntry["skillsSnapshot"];
|
skillsSnapshot?: SessionEntry["skillsSnapshot"];
|
||||||
@@ -63,6 +65,7 @@ export async function ensureSkillSnapshot(params: {
|
|||||||
isFirstTurnInSession,
|
isFirstTurnInSession,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
cfg,
|
cfg,
|
||||||
|
skillFilter,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let nextEntry = sessionEntry;
|
let nextEntry = sessionEntry;
|
||||||
@@ -76,7 +79,10 @@ export async function ensureSkillSnapshot(params: {
|
|||||||
};
|
};
|
||||||
const skillSnapshot =
|
const skillSnapshot =
|
||||||
isFirstTurnInSession || !current.skillsSnapshot
|
isFirstTurnInSession || !current.skillsSnapshot
|
||||||
? buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg })
|
? buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||||
|
config: cfg,
|
||||||
|
skillFilter,
|
||||||
|
})
|
||||||
: current.skillsSnapshot;
|
: current.skillsSnapshot;
|
||||||
nextEntry = {
|
nextEntry = {
|
||||||
...current,
|
...current,
|
||||||
@@ -96,7 +102,10 @@ export async function ensureSkillSnapshot(params: {
|
|||||||
nextEntry?.skillsSnapshot ??
|
nextEntry?.skillsSnapshot ??
|
||||||
(isFirstTurnInSession
|
(isFirstTurnInSession
|
||||||
? undefined
|
? undefined
|
||||||
: buildWorkspaceSkillSnapshot(workspaceDir, { config: cfg }));
|
: buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||||
|
config: cfg,
|
||||||
|
skillFilter,
|
||||||
|
}));
|
||||||
if (
|
if (
|
||||||
skillsSnapshot &&
|
skillsSnapshot &&
|
||||||
sessionStore &&
|
sessionStore &&
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type MsgContext = {
|
|||||||
GroupRoom?: string;
|
GroupRoom?: string;
|
||||||
GroupSpace?: string;
|
GroupSpace?: string;
|
||||||
GroupMembers?: string;
|
GroupMembers?: string;
|
||||||
|
GroupSystemPrompt?: string;
|
||||||
SenderName?: string;
|
SenderName?: string;
|
||||||
SenderId?: string;
|
SenderId?: string;
|
||||||
SenderUsername?: string;
|
SenderUsername?: string;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export type GetReplyOptions = {
|
|||||||
onBlockReply?: (payload: ReplyPayload) => Promise<void> | void;
|
onBlockReply?: (payload: ReplyPayload) => Promise<void> | void;
|
||||||
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
|
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
|
||||||
disableBlockStreaming?: boolean;
|
disableBlockStreaming?: boolean;
|
||||||
|
/** If provided, only load these skills for this session (empty = no skills). */
|
||||||
|
skillFilter?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReplyPayload = {
|
export type ReplyPayload = {
|
||||||
|
|||||||
Reference in New Issue
Block a user