"""LLM-backed draft synthesis for skill learning.""" from __future__ import annotations import json from typing import Any from beaver.engine.providers.base import LLMProvider from beaver.skills.learning.evidence import EvidencePacket from beaver.memory.skills.models import SkillLearningCandidate class SkillDraftSynthesizer: async def synthesize_revision( self, candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, provider: LLMProvider, model: str, ) -> dict[str, Any]: return await self._synthesize(candidate, evidence_packet, provider, model, "revise") async def synthesize_new_skill( self, candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, provider: LLMProvider, model: str, ) -> dict[str, Any]: return await self._synthesize(candidate, evidence_packet, provider, model, "new") async def synthesize_merge( self, candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, provider: LLMProvider, model: str, ) -> dict[str, Any]: return await self._synthesize(candidate, evidence_packet, provider, model, "merge") async def _synthesize( self, candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, provider: LLMProvider, model: str, action: str, ) -> dict[str, Any]: prompt = self._build_prompt(candidate, evidence_packet, action) 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." ), }, {"role": "user", "content": prompt}, ], tools=None, model=model, max_tokens=1500, temperature=0, ) payload = self._parse_payload(response.content or "") if payload: return payload return self._fallback_payload(candidate, evidence_packet, action) @staticmethod def _build_prompt(candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, action: str) -> str: return ( f"Action: {action}\n" f"Candidate kind: {candidate.kind}\n" f"Reason: {candidate.reason}\n" f"Related skills: {candidate.related_skill_names}\n" f"Task summaries:\n- " + "\n- ".join(evidence_packet.task_summaries) + "\n\nSession excerpts:\n" + "\n\n".join(evidence_packet.session_excerpts) + "\n\nReturn JSON only." ) @staticmethod def _parse_payload(content: str) -> dict[str, Any]: cleaned = content.strip() if cleaned.startswith("```"): lines = cleaned.splitlines() if len(lines) >= 3 and lines[0].startswith("```") and lines[-1].startswith("```"): cleaned = "\n".join(lines[1:-1]).strip() try: payload = json.loads(cleaned) except json.JSONDecodeError: return {} if not isinstance(payload, dict): return {} frontmatter = payload.get("frontmatter") content_value = payload.get("content") if not isinstance(frontmatter, dict) or not isinstance(content_value, str): return {} return { "frontmatter": frontmatter, "content": content_value.strip(), "change_reason": str(payload.get("change_reason") or ""), } @staticmethod def _fallback_payload(candidate: SkillLearningCandidate, evidence_packet: EvidencePacket, action: str) -> dict[str, Any]: related = candidate.related_skill_names[0] if candidate.related_skill_names else "generated-skill" title = related.replace("_", "-") content = "\n".join(f"- {item}" for item in evidence_packet.task_summaries[:5]) or "- No evidence captured." return { "frontmatter": { "description": candidate.reason or f"Auto-generated {action} draft for {title}.", "tools": [], }, "content": f"# {title}\n\n## Evidence\n\n{content}\n", "change_reason": candidate.reason or f"Fallback {action} synthesis.", }