From acec8ff74325430b9eae15afb8556c9b9e326671 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Sat, 6 Jun 2026 12:05:56 +0800 Subject: [PATCH] feat(evolution): Phase A - lifecycle hooks + EvolutionConfig U11: EvolutionMixin integrated into ConfigDrivenAgent lifecycle - on_task_complete triggers evolve_after_task - on_task_failed records failure patterns - Evolution errors never break main task flow U12: EvolutionConfig added to SkillConfig - enabled, reflect_on_failure, auto_apply, min_quality_threshold - Backward compatible: defaults to enabled=False 21 new tests passing, no regression. --- src/agentkit/core/config_driven.py | 64 +++- src/agentkit/skills/base.py | 19 ++ tests/unit/test_evolution_integration.py | 368 +++++++++++++++++++++++ 3 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_evolution_integration.py diff --git a/src/agentkit/core/config_driven.py b/src/agentkit/core/config_driven.py index 7de51d6..d683ea0 100644 --- a/src/agentkit/core/config_driven.py +++ b/src/agentkit/core/config_driven.py @@ -16,6 +16,8 @@ import yaml from agentkit.core.base import BaseAgent from agentkit.core.exceptions import ConfigValidationError from agentkit.core.protocol import AgentCapability, TaskMessage +from agentkit.evolution.lifecycle import EvolutionMixin +from agentkit.evolution.reflector import Reflector from agentkit.prompts.section import PromptSection from agentkit.prompts.template import PromptTemplate from agentkit.tools.base import Tool @@ -153,7 +155,7 @@ class AgentConfig: return d -class ConfigDrivenAgent(BaseAgent): +class ConfigDrivenAgent(BaseAgent, EvolutionMixin): """配置驱动的 Agent 从 YAML/Dict 配置自动组装,支持三种任务模式: @@ -247,6 +249,28 @@ class ConfigDrivenAgent(BaseAgent): from agentkit.quality.gate import QualityGate self._quality_gate = QualityGate() + # v2: Initialize Evolution if configured + evolution_config = getattr(config, 'evolution', None) + if evolution_config is not None: + # Support both dict and EvolutionConfig + if isinstance(evolution_config, dict): + is_enabled = evolution_config.get("enabled", False) + else: + is_enabled = getattr(evolution_config, 'enabled', False) + else: + is_enabled = False + + if is_enabled: + reflector = Reflector() + EvolutionMixin.__init__( + self, + reflector=reflector, + ) + self._evolution_enabled = True + else: + EvolutionMixin.__init__(self) # Initialize with no components + self._evolution_enabled = False + # v2: Initialize Output Standardizer from agentkit.quality.output import OutputStandardizer self._output_standardizer = OutputStandardizer() @@ -278,6 +302,44 @@ class ConfigDrivenAgent(BaseAgent): def prompt_template(self) -> PromptTemplate | None: return self._prompt_template + async def on_task_complete(self, task: TaskMessage, output: dict) -> None: + """Task complete hook - trigger evolution if enabled""" + if self._evolution_enabled: + try: + from agentkit.core.protocol import TaskResult, TaskStatus + from datetime import datetime, timezone + result = TaskResult( + task_id=task.task_id, + agent_name=self.name, + status=TaskStatus.COMPLETED, + output_data=output, + error_message=None, + started_at=datetime.now(timezone.utc), + completed_at=datetime.now(timezone.utc), + ) + await self.evolve_after_task(task, result) + except Exception as e: + logger.warning(f"Evolution after task failed: {e}") + + async def on_task_failed(self, task: TaskMessage, error: Exception) -> None: + """Task failed hook - record failure for evolution""" + if self._evolution_enabled: + try: + from agentkit.core.protocol import TaskResult, TaskStatus + from datetime import datetime, timezone + result = TaskResult( + task_id=task.task_id, + agent_name=self.name, + status=TaskStatus.FAILED, + output_data=None, + error_message=str(error), + started_at=datetime.now(timezone.utc), + completed_at=datetime.now(timezone.utc), + ) + await self.evolve_after_task(task, result) + except Exception as e: + logger.warning(f"Evolution after task failure failed: {e}") + def _bind_tools(self) -> None: """根据配置绑定工具""" for tool_name in self._config.tools: diff --git a/src/agentkit/skills/base.py b/src/agentkit/skills/base.py index 6e95ecb..919ff8f 100644 --- a/src/agentkit/skills/base.py +++ b/src/agentkit/skills/base.py @@ -11,6 +11,16 @@ from agentkit.tools.base import Tool logger = logging.getLogger(__name__) +@dataclass +class EvolutionConfig: + """Evolution configuration""" + + enabled: bool = False + reflect_on_failure: bool = True # Whether to reflect on failed tasks + auto_apply: bool = False # Whether to auto-apply optimizations (without AB test) + min_quality_threshold: float = 0.5 # Minimum quality score to trigger optimization + + @dataclass class IntentConfig: """意图配置""" @@ -59,6 +69,7 @@ class SkillConfig(AgentConfig): quality_gate: dict[str, Any] | None = None, execution_mode: str = "react", max_steps: int = 5, + evolution: dict[str, Any] | None = None, ): super().__init__( name=name, @@ -80,6 +91,7 @@ class SkillConfig(AgentConfig): self.quality_gate = QualityGateConfig(**(quality_gate or {})) self.execution_mode = execution_mode self.max_steps = max_steps + self.evolution = EvolutionConfig(**(evolution or {})) self._validate_v2() def _validate_v2(self) -> None: @@ -116,6 +128,7 @@ class SkillConfig(AgentConfig): quality_gate=data.get("quality_gate"), execution_mode=data.get("execution_mode", "react"), max_steps=data.get("max_steps", 5), + evolution=data.get("evolution"), ) @classmethod @@ -149,6 +162,12 @@ class SkillConfig(AgentConfig): } d["execution_mode"] = self.execution_mode d["max_steps"] = self.max_steps + d["evolution"] = { + "enabled": self.evolution.enabled, + "reflect_on_failure": self.evolution.reflect_on_failure, + "auto_apply": self.evolution.auto_apply, + "min_quality_threshold": self.evolution.min_quality_threshold, + } return d diff --git a/tests/unit/test_evolution_integration.py b/tests/unit/test_evolution_integration.py new file mode 100644 index 0000000..737efc9 --- /dev/null +++ b/tests/unit/test_evolution_integration.py @@ -0,0 +1,368 @@ +"""U11+U12 测试: Evolution 生命周期集成 + EvolutionConfig + +覆盖: +- EvolutionConfig 默认值与自定义值 +- SkillConfig 的 evolution 字段 +- ConfigDrivenAgent 集成 EvolutionMixin +- 生命周期钩子触发进化 +- 进化失败不影响主任务流程 +""" + +from datetime import datetime, timezone +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from agentkit.core.protocol import TaskMessage, TaskResult, TaskStatus + + +# ── Helpers ────────────────────────────────────────────── + + +def _make_task(**overrides) -> TaskMessage: + defaults = dict( + task_id="test-task-001", + agent_name="test_agent", + task_type="generate", + priority=1, + input_data={"query": "hello"}, + callback_url=None, + created_at=datetime.now(timezone.utc), + ) + defaults.update(overrides) + return TaskMessage(**defaults) + + +def _make_task_result(**overrides) -> TaskResult: + defaults = dict( + task_id="test-task-001", + agent_name="test_agent", + status=TaskStatus.COMPLETED, + output_data={"result": "ok"}, + error_message=None, + started_at=datetime.now(timezone.utc), + completed_at=datetime.now(timezone.utc), + ) + defaults.update(overrides) + return TaskResult(**defaults) + + +# ── EvolutionConfig 测试 ────────────────────────────────── + + +class TestEvolutionConfig: + """U12: EvolutionConfig 数据类测试""" + + def test_default_values(self): + """默认 EvolutionConfig — enabled=False""" + from agentkit.skills.base import EvolutionConfig + + config = EvolutionConfig() + assert config.enabled is False + assert config.reflect_on_failure is True + assert config.auto_apply is False + assert config.min_quality_threshold == 0.5 + + def test_from_dict_all_fields(self): + """EvolutionConfig 从字典创建 — 所有字段设置""" + from agentkit.skills.base import EvolutionConfig + + config = EvolutionConfig( + enabled=True, + reflect_on_failure=False, + auto_apply=True, + min_quality_threshold=0.8, + ) + assert config.enabled is True + assert config.reflect_on_failure is False + assert config.auto_apply is True + assert config.min_quality_threshold == 0.8 + + def test_from_dict_partial(self): + """EvolutionConfig 部分字段 — 缺失字段使用默认值""" + from agentkit.skills.base import EvolutionConfig + + config = EvolutionConfig(enabled=True) + assert config.enabled is True + assert config.reflect_on_failure is True # default + assert config.auto_apply is False # default + assert config.min_quality_threshold == 0.5 # default + + +# ── SkillConfig evolution 字段测试 ───────────────────────── + + +class TestSkillConfigEvolution: + """U12: SkillConfig 的 evolution 字段""" + + def test_skill_config_without_evolution(self): + """SkillConfig 无 evolution — 默认 enabled=False""" + from agentkit.skills.base import SkillConfig + + config = SkillConfig( + name="test_agent", + agent_type="test", + task_mode="llm_generate", + prompt={"identity": "test", "instructions": "test"}, + ) + assert config.evolution.enabled is False + + def test_skill_config_with_evolution(self): + """SkillConfig 有 evolution 配置 — 正确解析""" + from agentkit.skills.base import SkillConfig + + config = SkillConfig( + name="test_agent", + agent_type="test", + task_mode="llm_generate", + prompt={"identity": "test", "instructions": "test"}, + evolution={"enabled": True, "auto_apply": True, "min_quality_threshold": 0.7}, + ) + assert config.evolution.enabled is True + assert config.evolution.auto_apply is True + assert config.evolution.min_quality_threshold == 0.7 + + def test_skill_config_to_dict_includes_evolution(self): + """SkillConfig.to_dict 包含 evolution 字段""" + from agentkit.skills.base import SkillConfig + + config = SkillConfig( + name="test_agent", + agent_type="test", + task_mode="llm_generate", + prompt={"identity": "test", "instructions": "test"}, + evolution={"enabled": True}, + ) + d = config.to_dict() + assert "evolution" in d + assert d["evolution"]["enabled"] is True + assert d["evolution"]["reflect_on_failure"] is True + assert d["evolution"]["auto_apply"] is False + assert d["evolution"]["min_quality_threshold"] == 0.5 + + def test_skill_config_from_dict_with_evolution(self): + """SkillConfig.from_dict 正确解析 evolution""" + from agentkit.skills.base import SkillConfig + + data = { + "name": "test_agent", + "agent_type": "test", + "task_mode": "llm_generate", + "prompt": {"identity": "test", "instructions": "test"}, + "evolution": {"enabled": True, "reflect_on_failure": False}, + } + config = SkillConfig.from_dict(data) + assert config.evolution.enabled is True + assert config.evolution.reflect_on_failure is False + + +# ── ConfigDrivenAgent evolution 集成测试 ────────────────── + + +class TestConfigDrivenAgentEvolution: + """U11: ConfigDrivenAgent 集成 EvolutionMixin""" + + def _make_agent_config(self, evolution=None): + from agentkit.core.config_driven import AgentConfig + + config = AgentConfig( + name="test_agent", + agent_type="test", + task_mode="llm_generate", + prompt={"identity": "test", "instructions": "test"}, + ) + if evolution is not None: + config.evolution = evolution + return config + + def _make_skill_config(self, evolution=None): + from agentkit.skills.base import SkillConfig + + return SkillConfig( + name="test_agent", + agent_type="test", + task_mode="llm_generate", + prompt={"identity": "test", "instructions": "test"}, + evolution=evolution, + ) + + def test_agent_without_evolution_config(self): + """Agent 无 evolution 配置 — _evolution_enabled=False""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_agent_config() + agent = ConfigDrivenAgent(config=config) + assert agent._evolution_enabled is False + + def test_agent_with_evolution_enabled(self): + """Agent 有 evolution 且 enabled=True — _evolution_enabled=True""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_agent_config(evolution={"enabled": True}) + agent = ConfigDrivenAgent(config=config) + assert agent._evolution_enabled is True + + def test_agent_with_evolution_disabled(self): + """Agent 有 evolution 但 enabled=False — _evolution_enabled=False""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_agent_config(evolution={"enabled": False}) + agent = ConfigDrivenAgent(config=config) + assert agent._evolution_enabled is False + + async def test_on_task_complete_evolution_disabled(self): + """on_task_complete 进化禁用 — 不调用 evolve_after_task""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_agent_config() + agent = ConfigDrivenAgent(config=config) + + task = _make_task() + output = {"result": "ok"} + + # Should not raise and should not call evolve_after_task + await agent.on_task_complete(task, output) + + async def test_on_task_complete_evolution_enabled(self): + """on_task_complete 进化启用 — 调用 evolve_after_task""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_agent_config(evolution={"enabled": True}) + agent = ConfigDrivenAgent(config=config) + + task = _make_task() + output = {"result": "ok"} + + with patch.object(agent, "evolve_after_task", new_callable=AsyncMock) as mock_evolve: + await agent.on_task_complete(task, output) + mock_evolve.assert_called_once() + # Verify the TaskResult passed to evolve_after_task + call_args = mock_evolve.call_args + result_arg = call_args[0][1] # second positional arg is TaskResult + assert result_arg.status == TaskStatus.COMPLETED + assert result_arg.output_data == output + + async def test_on_task_failed_evolution_enabled(self): + """on_task_failed 进化启用 — 调用 evolve_after_task""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_agent_config(evolution={"enabled": True}) + agent = ConfigDrivenAgent(config=config) + + task = _make_task() + error = ValueError("test error") + + with patch.object(agent, "evolve_after_task", new_callable=AsyncMock) as mock_evolve: + await agent.on_task_failed(task, error) + mock_evolve.assert_called_once() + # Verify the TaskResult passed to evolve_after_task + call_args = mock_evolve.call_args + result_arg = call_args[0][1] # second positional arg is TaskResult + assert result_arg.status == TaskStatus.FAILED + assert result_arg.error_message == "test error" + + async def test_evolution_failure_does_not_break_task(self): + """进化失败不影响任务完成""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_agent_config(evolution={"enabled": True}) + agent = ConfigDrivenAgent(config=config) + + task = _make_task() + output = {"result": "ok"} + + with patch.object(agent, "evolve_after_task", new_callable=AsyncMock, side_effect=RuntimeError("evolution crashed")): + # Should NOT raise — evolution failure is caught + await agent.on_task_complete(task, output) + + async def test_evolution_failure_on_task_failed_does_not_break(self): + """进化失败不影响 on_task_failed""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_agent_config(evolution={"enabled": True}) + agent = ConfigDrivenAgent(config=config) + + task = _make_task() + error = ValueError("task error") + + with patch.object(agent, "evolve_after_task", new_callable=AsyncMock, side_effect=RuntimeError("evolution crashed")): + # Should NOT raise + await agent.on_task_failed(task, error) + + def test_skill_config_evolution_propagated(self): + """SkillConfig 的 evolution 配置传递到 ConfigDrivenAgent""" + from agentkit.core.config_driven import ConfigDrivenAgent + + config = self._make_skill_config(evolution={"enabled": True}) + agent = ConfigDrivenAgent(config=config) + assert agent._evolution_enabled is True + + +# ── EvolutionMixin 集成测试 ─────────────────────────────── + + +class TestEvolutionMixinIntegration: + """U11: EvolutionMixin 方法集成到 ConfigDrivenAgent""" + + def _make_agent_with_evolution(self): + from agentkit.core.config_driven import AgentConfig, ConfigDrivenAgent + + config = AgentConfig( + name="test_agent", + agent_type="test", + task_mode="llm_generate", + prompt={"identity": "test", "instructions": "test"}, + ) + config.evolution = {"enabled": True} + return ConfigDrivenAgent(config=config) + + def test_agent_has_get_evolution_history(self): + """Agent 继承 get_evolution_history 方法""" + from agentkit.core.config_driven import ConfigDrivenAgent + from agentkit.evolution.lifecycle import EvolutionMixin + + agent = self._make_agent_with_evolution() + assert hasattr(agent, "get_evolution_history") + assert callable(agent.get_evolution_history) + + def test_agent_has_set_current_module(self): + """Agent 继承 set_current_module 方法""" + from agentkit.core.config_driven import ConfigDrivenAgent + from agentkit.evolution.lifecycle import EvolutionMixin + + agent = self._make_agent_with_evolution() + assert hasattr(agent, "set_current_module") + assert callable(agent.set_current_module) + + def test_get_evolution_history_empty_initially(self): + """get_evolution_history 初始返回空列表""" + agent = self._make_agent_with_evolution() + history = agent.get_evolution_history() + assert history == [] + + def test_set_current_module_works(self): + """set_current_module 正常工作""" + from agentkit.evolution.prompt_optimizer import Module, Signature + + agent = self._make_agent_with_evolution() + signature = Signature( + input_fields={"query": "user query"}, + output_fields={"result": "result"}, + instruction="test instructions", + ) + module = Module(name="test_module", signature=signature) + agent.set_current_module(module) + assert agent._current_module is not None + assert agent._current_module.name == "test_module" + + def test_mro_correct(self): + """MRO 正确: ConfigDrivenAgent → BaseAgent → EvolutionMixin""" + from agentkit.core.config_driven import ConfigDrivenAgent + from agentkit.core.base import BaseAgent + from agentkit.evolution.lifecycle import EvolutionMixin + + mro = ConfigDrivenAgent.__mro__ + # BaseAgent should come before EvolutionMixin in MRO + base_idx = mro.index(BaseAgent) + mixin_idx = mro.index(EvolutionMixin) + assert base_idx < mixin_idx, f"BaseAgent (idx={base_idx}) should come before EvolutionMixin (idx={mixin_idx}) in MRO"