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.
This commit is contained in:
parent
2844eeb548
commit
acec8ff743
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue