feat(beaver): 完成Task Team功能v1实现,重构后端架构支持统一内核
新增内部Task系统,包括验证、反馈门控机制,实现自动质量验证 (通过率>=0.75)和用户反馈闭环(satisfied/revise/abandon)。 实现Agent Team v1协调器,支持sequence/parallel/dag执行策略, sub-agent复用主AgentLoop,每个run使用独立memory snapshot。 建立Skill学习pipeline,包含draft/审核/发布/回滚完整生命周期, 通过Task验证通过且用户满意才生成学习候选。 重构目录结构,移除third_party依赖,建立统一engine内核, 所有agent共享运行时基础组件。 更新ContextBuilder清理provider消息字段,增强SkillContext版本管理, 集成TaskExecutionPlanner和TaskSkillResolver实现技能解析机制。
This commit is contained in:
354
app-instance/backend/beaver/skills/learning/pipeline.py
Normal file
354
app-instance/backend/beaver/skills/learning/pipeline.py
Normal file
@ -0,0 +1,354 @@
|
||||
"""Manual skill learning pipeline orchestration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from beaver.engine.providers import ProviderBundle
|
||||
from beaver.memory.skills import SkillDraftEvalReport, SkillDraftSafetyReport, SkillLearningCandidate, SkillLearningStore
|
||||
from beaver.skills.drafts import DraftService
|
||||
from beaver.skills.learning.eval import SkillDraftEvaluator
|
||||
from beaver.skills.learning.service import SkillLearningService
|
||||
from beaver.skills.learning.safety import SkillDraftSafetyChecker
|
||||
from beaver.skills.publisher import SkillPublisher
|
||||
from beaver.skills.reviews import ReviewService
|
||||
from beaver.skills.specs import SkillDraft, SkillReviewRecord, SkillReviewState, SkillSpec, SkillVersion
|
||||
|
||||
|
||||
class SkillLearningPipelineService:
|
||||
"""Coordinates candidate -> draft -> review -> publish lifecycle."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
learning_store: SkillLearningStore,
|
||||
learning_service: SkillLearningService,
|
||||
draft_service: DraftService,
|
||||
review_service: ReviewService,
|
||||
publisher: SkillPublisher,
|
||||
safety_checker: SkillDraftSafetyChecker | None = None,
|
||||
evaluator: SkillDraftEvaluator | None = None,
|
||||
) -> None:
|
||||
self.learning_store = learning_store
|
||||
self.learning_service = learning_service
|
||||
self.draft_service = draft_service
|
||||
self.review_service = review_service
|
||||
self.publisher = publisher
|
||||
self.safety_checker = safety_checker or SkillDraftSafetyChecker()
|
||||
self.evaluator = evaluator
|
||||
|
||||
def list_candidates(self, status: str | None = None) -> list[SkillLearningCandidate]:
|
||||
return self.learning_store.list_learning_candidates(status=status)
|
||||
|
||||
def get_candidate(self, candidate_id: str) -> SkillLearningCandidate:
|
||||
for candidate in self.learning_store.list_learning_candidates():
|
||||
if candidate.candidate_id == candidate_id:
|
||||
return candidate
|
||||
raise ValueError(f"Unknown learning candidate: {candidate_id}")
|
||||
|
||||
async def synthesize_draft(
|
||||
self,
|
||||
candidate_id: str,
|
||||
*,
|
||||
provider_bundle: ProviderBundle,
|
||||
) -> SkillDraft:
|
||||
draft = await self.learning_service.synthesize_draft(candidate_id, provider_bundle)
|
||||
self.mark_draft_synthesized(candidate_id, draft)
|
||||
return draft
|
||||
|
||||
async def regenerate_draft(
|
||||
self,
|
||||
candidate_id: str,
|
||||
*,
|
||||
provider_bundle: ProviderBundle,
|
||||
) -> SkillDraft:
|
||||
self.learning_store.transition_learning_candidate(
|
||||
candidate_id,
|
||||
"synthesizing",
|
||||
event_type="draft_synthesis_started",
|
||||
last_error=None,
|
||||
)
|
||||
return await self.synthesize_draft(candidate_id, provider_bundle=provider_bundle)
|
||||
|
||||
def mark_candidate_queued(self, candidate_id: str) -> SkillLearningCandidate:
|
||||
return self._require_updated(
|
||||
self.learning_store.transition_learning_candidate(
|
||||
candidate_id,
|
||||
"queued",
|
||||
event_type="candidate_queued",
|
||||
last_error=None,
|
||||
),
|
||||
candidate_id,
|
||||
)
|
||||
|
||||
def mark_candidate_synthesizing(self, candidate_id: str) -> SkillLearningCandidate:
|
||||
return self._require_updated(
|
||||
self.learning_store.transition_learning_candidate(
|
||||
candidate_id,
|
||||
"synthesizing",
|
||||
event_type="draft_synthesis_started",
|
||||
last_error=None,
|
||||
),
|
||||
candidate_id,
|
||||
)
|
||||
|
||||
def mark_draft_synthesized(self, candidate_id: str, draft: SkillDraft) -> SkillLearningCandidate:
|
||||
candidate = self.get_candidate(candidate_id)
|
||||
evidence = dict(candidate.evidence)
|
||||
evidence["draft_id"] = draft.draft_id
|
||||
evidence["draft_skill_name"] = draft.skill_name
|
||||
return self._require_updated(
|
||||
self.learning_store.transition_learning_candidate(
|
||||
candidate_id,
|
||||
"draft_ready",
|
||||
event_type="draft_synthesis_completed",
|
||||
evidence=evidence,
|
||||
draft_id=draft.draft_id,
|
||||
draft_skill_name=draft.skill_name,
|
||||
risk_level=candidate.risk_level,
|
||||
last_error=None,
|
||||
payload={"draft_id": draft.draft_id, "skill_name": draft.skill_name},
|
||||
),
|
||||
candidate_id,
|
||||
)
|
||||
|
||||
def mark_candidate_failed(
|
||||
self,
|
||||
candidate_id: str,
|
||||
error: str,
|
||||
*,
|
||||
retry_count: int,
|
||||
terminal: bool,
|
||||
) -> SkillLearningCandidate:
|
||||
return self._require_updated(
|
||||
self.learning_store.transition_learning_candidate(
|
||||
candidate_id,
|
||||
"failed" if terminal else "open",
|
||||
event_type="failed",
|
||||
retry_count=retry_count,
|
||||
last_error=error,
|
||||
payload={"error": error, "terminal": terminal, "retry_count": retry_count},
|
||||
),
|
||||
candidate_id,
|
||||
)
|
||||
|
||||
def mark_candidate_superseded(self, candidate_id: str, reason: str) -> SkillLearningCandidate:
|
||||
return self._require_updated(
|
||||
self.learning_store.transition_learning_candidate(
|
||||
candidate_id,
|
||||
"superseded",
|
||||
event_type="superseded",
|
||||
last_error=reason,
|
||||
payload={"reason": reason},
|
||||
),
|
||||
candidate_id,
|
||||
)
|
||||
|
||||
def list_drafts(self, skill_name: str | None = None) -> list[SkillDraft]:
|
||||
return self.draft_service.list_drafts(skill_name)
|
||||
|
||||
def get_draft(self, skill_name: str, draft_id: str) -> SkillDraft:
|
||||
draft = self.draft_service.get_draft(skill_name, draft_id)
|
||||
if draft is None:
|
||||
raise ValueError(f"Draft not found: {skill_name}/{draft_id}")
|
||||
return draft
|
||||
|
||||
def submit_review(
|
||||
self,
|
||||
skill_name: str,
|
||||
draft_id: str,
|
||||
*,
|
||||
requested_by: str = "system",
|
||||
notes: str = "",
|
||||
) -> SkillReviewRecord:
|
||||
safety = self.get_safety_report(skill_name, draft_id)
|
||||
if safety is not None and (not safety.passed or safety.risk_level == "critical"):
|
||||
raise ValueError("Draft cannot enter review because safety check failed")
|
||||
return self.review_service.submit_for_review(
|
||||
skill_name,
|
||||
draft_id,
|
||||
reviewer_request=notes,
|
||||
requested_by=requested_by,
|
||||
)
|
||||
|
||||
def approve(
|
||||
self,
|
||||
skill_name: str,
|
||||
draft_id: str,
|
||||
*,
|
||||
reviewer: str = "system",
|
||||
notes: str = "",
|
||||
) -> SkillReviewRecord:
|
||||
review = self.review_service.approve(skill_name, draft_id, reviewer=reviewer, notes=notes)
|
||||
self._mark_candidate_by_draft(skill_name, draft_id, "approved", "approved")
|
||||
return review
|
||||
|
||||
def reject(
|
||||
self,
|
||||
skill_name: str,
|
||||
draft_id: str,
|
||||
*,
|
||||
reviewer: str = "system",
|
||||
notes: str = "",
|
||||
) -> SkillReviewRecord:
|
||||
review = self.review_service.reject(skill_name, draft_id, reviewer=reviewer, notes=notes)
|
||||
self._mark_candidate_by_draft(skill_name, draft_id, "rejected", "rejected")
|
||||
return review
|
||||
|
||||
def publish(
|
||||
self,
|
||||
skill_name: str,
|
||||
draft_id: str,
|
||||
*,
|
||||
publisher: str = "system",
|
||||
notes: str = "",
|
||||
confirm_high_risk: bool = False,
|
||||
) -> SkillVersion | SkillSpec:
|
||||
draft = self.get_draft(skill_name, draft_id)
|
||||
self._validate_publish_gates(draft, confirm_high_risk=confirm_high_risk)
|
||||
if draft.proposal_kind == "retire_skill":
|
||||
result = self.publisher.apply_retire_proposal(skill_name, draft_id, actor=publisher, notes=notes)
|
||||
else:
|
||||
result = self.publisher.publish(skill_name, draft_id, publisher=publisher, notes=notes)
|
||||
self._mark_candidate_by_draft(skill_name, draft_id, "published", "published")
|
||||
return result
|
||||
|
||||
def rollback(
|
||||
self,
|
||||
skill_name: str,
|
||||
target_version: str,
|
||||
*,
|
||||
actor: str = "system",
|
||||
reason: str = "",
|
||||
) -> SkillSpec:
|
||||
return self.publisher.rollback(skill_name, target_version, actor=actor, reason=reason or "manual rollback")
|
||||
|
||||
def disable(
|
||||
self,
|
||||
skill_name: str,
|
||||
*,
|
||||
actor: str = "system",
|
||||
reason: str = "",
|
||||
) -> SkillSpec:
|
||||
return self.publisher.disable(skill_name, actor=actor, reason=reason or "manual disable")
|
||||
|
||||
def reviews_for_draft(self, skill_name: str, draft_id: str) -> list[SkillReviewRecord]:
|
||||
return self.review_service.store.list_reviews(skill_name, draft_id=draft_id)
|
||||
|
||||
def check_safety(self, skill_name: str, draft_id: str) -> SkillDraftSafetyReport:
|
||||
draft = self.get_draft(skill_name, draft_id)
|
||||
report = self.safety_checker.check(draft)
|
||||
self.learning_store.write_safety_report(report)
|
||||
status = "safety_failed" if not report.passed or report.risk_level == "critical" else "draft_ready"
|
||||
current = self._candidate_by_draft(skill_name, draft_id)
|
||||
if current is not None and current.status == "eval_failed" and status == "draft_ready":
|
||||
status = "eval_failed"
|
||||
self._mark_candidate_by_draft(
|
||||
skill_name,
|
||||
draft_id,
|
||||
status,
|
||||
"safety_checked",
|
||||
safety_report_id=report.report_id,
|
||||
risk_level=report.risk_level,
|
||||
last_error="; ".join(report.blocked_reasons) if status == "safety_failed" else None,
|
||||
)
|
||||
return report
|
||||
|
||||
def get_safety_report(self, skill_name: str, draft_id: str) -> SkillDraftSafetyReport | None:
|
||||
return self.learning_store.get_safety_report(skill_name, draft_id)
|
||||
|
||||
def get_eval_report(self, skill_name: str, draft_id: str) -> SkillDraftEvalReport | None:
|
||||
return self.learning_store.get_eval_report(skill_name, draft_id)
|
||||
|
||||
async def evaluate_draft(
|
||||
self,
|
||||
candidate_id: str,
|
||||
skill_name: str,
|
||||
draft_id: str,
|
||||
*,
|
||||
provider_bundle: ProviderBundle | None,
|
||||
) -> SkillDraftEvalReport:
|
||||
draft = self.get_draft(skill_name, draft_id)
|
||||
candidate = self.get_candidate(candidate_id)
|
||||
evaluator = self.evaluator or SkillDraftEvaluator(self.learning_service.run_store)
|
||||
report = await evaluator.evaluate(candidate=candidate, draft=draft, provider_bundle=provider_bundle)
|
||||
self.learning_store.write_eval_report(report)
|
||||
if report.status == "skipped_provider_unavailable":
|
||||
status = "draft_ready"
|
||||
error = "eval skipped: provider unavailable"
|
||||
elif report.passed:
|
||||
status = "draft_ready"
|
||||
error = None
|
||||
else:
|
||||
status = "eval_failed"
|
||||
error = "eval failed"
|
||||
current = self._candidate_by_draft(skill_name, draft_id)
|
||||
if current is not None and current.status == "safety_failed" and status == "draft_ready":
|
||||
status = "safety_failed"
|
||||
error = current.last_error
|
||||
self.learning_store.transition_learning_candidate(
|
||||
candidate_id,
|
||||
status,
|
||||
event_type="eval_completed",
|
||||
eval_report_id=report.report_id,
|
||||
last_error=error,
|
||||
payload=report.to_dict(),
|
||||
)
|
||||
return report
|
||||
|
||||
def _validate_publish_gates(self, draft: SkillDraft, *, confirm_high_risk: bool) -> None:
|
||||
reviews = self.reviews_for_draft(draft.skill_name, draft.draft_id)
|
||||
if not any(review.status == SkillReviewState.APPROVED.value for review in reviews):
|
||||
raise ValueError("Draft must have an approved review before publish")
|
||||
safety = self.get_safety_report(draft.skill_name, draft.draft_id)
|
||||
if safety is None:
|
||||
raise ValueError("Draft requires a passing safety report before publish")
|
||||
if not safety.passed:
|
||||
raise ValueError("Draft safety report did not pass")
|
||||
if safety.risk_level == "critical":
|
||||
raise ValueError("Critical risk drafts cannot be published")
|
||||
if safety.risk_level == "high" and not confirm_high_risk:
|
||||
raise ValueError("High risk draft publish requires confirm_high_risk=true")
|
||||
eval_report = self.get_eval_report(draft.skill_name, draft.draft_id)
|
||||
if eval_report is not None and eval_report.status != "skipped_provider_unavailable" and not eval_report.passed:
|
||||
raise ValueError("Draft eval report did not pass")
|
||||
|
||||
def _mark_candidate_by_draft(
|
||||
self,
|
||||
skill_name: str,
|
||||
draft_id: str,
|
||||
status: str,
|
||||
event_type: str,
|
||||
**updates: object,
|
||||
) -> SkillLearningCandidate | None:
|
||||
candidate = self._candidate_by_draft(skill_name, draft_id)
|
||||
if candidate is None:
|
||||
return None
|
||||
if candidate.status in {"safety_failed", "eval_failed"} and status in {"review_pending", "approved"}:
|
||||
return candidate
|
||||
return self.learning_store.transition_learning_candidate(
|
||||
candidate.candidate_id,
|
||||
status,
|
||||
event_type=event_type,
|
||||
**updates,
|
||||
)
|
||||
|
||||
def _candidate_by_draft(self, skill_name: str, draft_id: str) -> SkillLearningCandidate | None:
|
||||
for candidate in self.learning_store.list_learning_candidates():
|
||||
if candidate.draft_skill_name == skill_name and candidate.draft_id == draft_id:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _require_updated(candidate: SkillLearningCandidate | None, candidate_id: str) -> SkillLearningCandidate:
|
||||
if candidate is None:
|
||||
raise ValueError(f"Unknown learning candidate: {candidate_id}")
|
||||
return candidate
|
||||
|
||||
|
||||
def model_to_dict(value: Any) -> dict[str, Any]:
|
||||
if hasattr(value, "to_dict"):
|
||||
return value.to_dict()
|
||||
if isinstance(value, dict):
|
||||
return dict(value)
|
||||
raise TypeError(f"Cannot convert {type(value).__name__} to dict")
|
||||
Reference in New Issue
Block a user