From beddf12bc0c8ebc286b36db2a892106ef778510a Mon Sep 17 00:00:00 2001 From: steven_li Date: Mon, 15 Jun 2026 18:00:59 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(learning):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E8=BF=90=E8=A1=8C=E8=AE=B0=E5=BD=95=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E9=80=BB=E8=BE=91=E5=A4=84=E7=90=86=E7=A9=BAattempt?= =?UTF-8?q?=5Findex=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当RunRecord的attempt_index为None时,之前的排序逻辑会出现问题。 现在通过在排序键中显式处理None值来解决这个问题, 将None值排在前面,并将其转换为0进行比较。 同时添加了单元测试验证团队运行记录(没有attempt_index)的处理情况。 ``` --- .../backend/beaver/skills/learning/service.py | 10 ++++- .../tests/unit/test_phase5_skills_runtime.py | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app-instance/backend/beaver/skills/learning/service.py b/app-instance/backend/beaver/skills/learning/service.py index 3fdf7e2..83d80c6 100644 --- a/app-instance/backend/beaver/skills/learning/service.py +++ b/app-instance/backend/beaver/skills/learning/service.py @@ -462,7 +462,15 @@ class SkillLearningService: @staticmethod def _representative_task_text(runs: list[RunRecord], *, fallback: str = "") -> str: - ordered = sorted(runs, key=lambda item: (item.attempt_index, item.started_at, item.run_id)) + ordered = sorted( + runs, + key=lambda item: ( + item.attempt_index is None, + item.attempt_index if item.attempt_index is not None else 0, + item.started_at, + item.run_id, + ), + ) for record in ordered: text = record.task_text.strip() if text: diff --git a/app-instance/backend/tests/unit/test_phase5_skills_runtime.py b/app-instance/backend/tests/unit/test_phase5_skills_runtime.py index 4d45e66..22c1041 100644 --- a/app-instance/backend/tests/unit/test_phase5_skills_runtime.py +++ b/app-instance/backend/tests/unit/test_phase5_skills_runtime.py @@ -591,6 +591,51 @@ def test_skill_learning_service_uses_original_task_text_for_new_skill_theme(tmp_ assert candidates[0].evidence["task_text"] == "Compare direct production restart with staging rollout" +def test_skill_learning_service_handles_team_runs_without_attempt_index(tmp_path: Path) -> None: + store = SkillSpecStore(tmp_path) + run_store = RunMemoryStore(tmp_path / "memory" / "runs") + learning_store = SkillLearningStore(tmp_path / "memory" / "skills") + service = SkillLearningService( + run_store=run_store, + learning_store=learning_store, + draft_service=DraftService(store), + evidence_selector=EvidenceSelector(run_store), + ) + now = datetime.now(timezone.utc).isoformat() + run_store.append_run_record( + RunRecord( + run_id="team-run", + session_id="session-task:team:research", + task_id="task-1", + attempt_index=None, + task_text="Research one product", + started_at=now, + ended_at=now, + success=True, + finish_reason="stop", + ) + ) + run_store.append_run_record( + RunRecord( + run_id="main-run", + session_id="session-task", + task_id="task-1", + attempt_index=1, + task_text="Compare two products and email the report", + started_at=now, + ended_at=now, + success=True, + finish_reason="stop", + feedback={"acceptance_type": "accept"}, + ) + ) + + candidates = service.build_learning_candidates_for_task("task-1", final_accepted_run_id="main-run") + + assert [candidate.candidate_id for candidate in candidates] == ["new:task:task-1"] + assert candidates[0].evidence["task_text"] == "Compare two products and email the report" + + def test_task_theme_uses_first_sentence_for_chinese_text() -> None: assert ( SkillLearningService._task_theme(