feat(skill-learning): preserve base skill during synthesis
This commit is contained in:
@ -17,8 +17,9 @@ class SkillDraftSynthesizer:
|
||||
evidence_packet: EvidencePacket,
|
||||
provider: LLMProvider,
|
||||
model: str,
|
||||
base_skill: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return await self._synthesize(candidate, evidence_packet, provider, model, "revise")
|
||||
return await self._synthesize(candidate, evidence_packet, provider, model, "revise", base_skill=base_skill)
|
||||
|
||||
async def synthesize_new_skill(
|
||||
self,
|
||||
@ -27,7 +28,7 @@ class SkillDraftSynthesizer:
|
||||
provider: LLMProvider,
|
||||
model: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self._synthesize(candidate, evidence_packet, provider, model, "new")
|
||||
return await self._synthesize(candidate, evidence_packet, provider, model, "new", base_skill=None)
|
||||
|
||||
async def synthesize_merge(
|
||||
self,
|
||||
@ -35,8 +36,9 @@ class SkillDraftSynthesizer:
|
||||
evidence_packet: EvidencePacket,
|
||||
provider: LLMProvider,
|
||||
model: str,
|
||||
base_skill: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return await self._synthesize(candidate, evidence_packet, provider, model, "merge")
|
||||
return await self._synthesize(candidate, evidence_packet, provider, model, "merge", base_skill=base_skill)
|
||||
|
||||
async def _synthesize(
|
||||
self,
|
||||
@ -45,15 +47,18 @@ class SkillDraftSynthesizer:
|
||||
provider: LLMProvider,
|
||||
model: str,
|
||||
action: str,
|
||||
*,
|
||||
base_skill: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
prompt = self._build_prompt(candidate, evidence_packet, action)
|
||||
prompt = self._build_prompt(candidate, evidence_packet, action, base_skill=base_skill)
|
||||
response = await provider.chat(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You synthesize Beaver skill drafts from execution evidence. "
|
||||
"Return only JSON with keys: frontmatter, content, change_reason."
|
||||
"Return only JSON with keys: frontmatter, content, change_reason, "
|
||||
"preserved_sections, changed_sections, dropped_sections."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
@ -69,11 +74,30 @@ class SkillDraftSynthesizer:
|
||||
return self._fallback_payload(candidate, evidence_packet, action)
|
||||
|
||||
@staticmethod
|
||||
def _build_prompt(candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, action: str) -> str:
|
||||
def _build_prompt(
|
||||
candidate: SkillLearningCandidate,
|
||||
evidence_packet: EvidencePacket,
|
||||
action: str,
|
||||
base_skill: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
tool_names = _coerce_string_list(evidence_packet.metadata.get("tool_names"))
|
||||
tool_section = ", ".join(tool_names) if tool_names else "none observed"
|
||||
selected_tool_names = _coerce_string_list(evidence_packet.metadata.get("selected_tool_names"))
|
||||
selected_tool_section = ", ".join(selected_tool_names) if selected_tool_names else "none recorded"
|
||||
base_section = ""
|
||||
if base_skill:
|
||||
base_section = (
|
||||
"\n\nBase skill snapshot:\n"
|
||||
f"- skill_name: {base_skill.get('skill_name')}\n"
|
||||
f"- version: {base_skill.get('version')}\n"
|
||||
f"- frontmatter: {json.dumps(base_skill.get('frontmatter') or {}, ensure_ascii=False, sort_keys=True)}\n"
|
||||
f"- tool_hints: {base_skill.get('tool_hints') or []}\n"
|
||||
f"- summary: {base_skill.get('summary') or ''}\n"
|
||||
"Base skill content:\n"
|
||||
f"{base_skill.get('content') or ''}\n"
|
||||
"Preserve existing instructions unless the evidence requires a change. "
|
||||
"If any section is changed or dropped, explain it in changed_sections or dropped_sections."
|
||||
)
|
||||
return (
|
||||
f"Action: {action}\n"
|
||||
f"Candidate kind: {candidate.kind}\n"
|
||||
@ -83,11 +107,13 @@ class SkillDraftSynthesizer:
|
||||
f"Run-selected tool names: {selected_tool_section}\n"
|
||||
f"Task summaries:\n- " + "\n- ".join(evidence_packet.task_summaries)
|
||||
+ "\n\nSession excerpts:\n" + "\n\n".join(evidence_packet.session_excerpts)
|
||||
+ base_section
|
||||
+ "\n\nReturn JSON only. The frontmatter object must include:"
|
||||
+ "\n- description: a concise skill description"
|
||||
+ "\n- tools: an explicit JSON array of exact tool names this skill needs. "
|
||||
+ "Prefer called tool names when the workflow depends on them; use run-selected tool names only when clearly required. "
|
||||
+ "Use [] only when no tool is required."
|
||||
+ "\nThe JSON may include preserved_sections, changed_sections, and dropped_sections arrays."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@ -111,6 +137,9 @@ class SkillDraftSynthesizer:
|
||||
"frontmatter": frontmatter,
|
||||
"content": content_value.strip(),
|
||||
"change_reason": str(payload.get("change_reason") or ""),
|
||||
"preserved_sections": _coerce_string_list(payload.get("preserved_sections")),
|
||||
"changed_sections": _coerce_string_list(payload.get("changed_sections")),
|
||||
"dropped_sections": _coerce_string_list(payload.get("dropped_sections")),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@ -124,6 +153,9 @@ class SkillDraftSynthesizer:
|
||||
"frontmatter": frontmatter,
|
||||
"content": str(payload.get("content") or "").strip(),
|
||||
"change_reason": str(payload.get("change_reason") or ""),
|
||||
"preserved_sections": _coerce_string_list(payload.get("preserved_sections")),
|
||||
"changed_sections": _coerce_string_list(payload.get("changed_sections")),
|
||||
"dropped_sections": _coerce_string_list(payload.get("dropped_sections")),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@ -138,6 +170,9 @@ class SkillDraftSynthesizer:
|
||||
},
|
||||
"content": f"# {title}\n\n## Evidence\n\n{content}\n",
|
||||
"change_reason": candidate.reason or f"Fallback {action} synthesis.",
|
||||
"preserved_sections": [],
|
||||
"changed_sections": [],
|
||||
"dropped_sections": [],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user