fix(actor): add v3 YAML text generation for built-in actors
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 56s
CI / quality (pull_request) Successful in 1m12s
CI / typecheck (pull_request) Successful in 1m26s
CI / security (pull_request) Successful in 1m20s
CI / build (pull_request) Successful in 38s
CI / helm (pull_request) Successful in 36s
CI / push-validation (pull_request) Successful in 36s
CI / integration_tests (pull_request) Successful in 3m10s
CI / e2e_tests (pull_request) Successful in 3m21s
CI / unit_tests (pull_request) Successful in 4m31s
CI / docker (pull_request) Successful in 1m31s
CI / coverage (pull_request) Successful in 11m45s
CI / status-check (pull_request) Successful in 3s
CI / status-check (push) Blocked by required conditions
CI / push-validation (push) Successful in 37s
CI / helm (push) Successful in 41s
CI / benchmark-publish (push) Failing after 56s
CI / build (push) Successful in 1m0s
CI / lint (push) Successful in 1m35s
CI / quality (push) Successful in 1m37s
CI / typecheck (push) Successful in 1m59s
CI / security (push) Successful in 2m0s
CI / e2e_tests (push) Successful in 3m53s
CI / integration_tests (push) Successful in 4m44s
CI / unit_tests (push) Successful in 5m27s
CI / docker (push) Successful in 1m30s
CI / coverage (push) Failing after 22m54s
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 56s
CI / quality (pull_request) Successful in 1m12s
CI / typecheck (pull_request) Successful in 1m26s
CI / security (pull_request) Successful in 1m20s
CI / build (pull_request) Successful in 38s
CI / helm (pull_request) Successful in 36s
CI / push-validation (pull_request) Successful in 36s
CI / integration_tests (pull_request) Successful in 3m10s
CI / e2e_tests (pull_request) Successful in 3m21s
CI / unit_tests (pull_request) Successful in 4m31s
CI / docker (pull_request) Successful in 1m31s
CI / coverage (pull_request) Successful in 11m45s
CI / status-check (pull_request) Successful in 3s
CI / status-check (push) Blocked by required conditions
CI / push-validation (push) Successful in 37s
CI / helm (push) Successful in 41s
CI / benchmark-publish (push) Failing after 56s
CI / build (push) Successful in 1m0s
CI / lint (push) Successful in 1m35s
CI / quality (push) Successful in 1m37s
CI / typecheck (push) Successful in 1m59s
CI / security (push) Successful in 2m0s
CI / e2e_tests (push) Successful in 3m53s
CI / integration_tests (push) Successful in 4m44s
CI / unit_tests (push) Successful in 5m27s
CI / docker (push) Successful in 1m30s
CI / coverage (push) Failing after 22m54s
Built-in actors (e.g., openai/gpt-4, anthropic/claude-3-opus) were failing silently with agents actor run because they lacked the required v3 type field in their stored configuration. The ReactiveConfigParser._is_v3_format() check failed, resulting in empty agents/routes dictionaries and no output. This fix adds _generate_builtin_actor_yaml() helper to ActorRegistry that generates spec-compliant v3 YAML text including: - type: llm (required for v3 format recognition) - description (required by v3 schema) - name, model, provider, capabilities, unsafe, source fields The ensure_built_in_actors() method now calls this helper and persists yaml_text via upsert_actor(), ensuring built-in actors work identically to custom actors with the agents actor run command. Existing built-in actors will be automatically refreshed on next startup since they are regenerated from the provider registry - no database migration needed. Added: - _generate_builtin_actor_yaml() helper method - BDD feature file with scenarios for v3 YAML format - Step definitions for new BDD scenarios - Unit tests covering YAML generation and schema validation - CHANGELOG entry ISSUES CLOSED: #10883
This commit was merged in pull request #10894.
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
"""Unit tests for built-in actor v3 YAML generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from cleveragents.actor.registry import ActorRegistry
|
||||
from cleveragents.actor.schema import ActorConfigSchema, ActorType, is_v3_yaml
|
||||
from cleveragents.application.services.actor_service import ActorService
|
||||
from cleveragents.config.settings import ProviderDefaults, Settings
|
||||
from cleveragents.providers.registry import (
|
||||
ProviderCapabilities,
|
||||
ProviderInfo,
|
||||
ProviderRegistry,
|
||||
ProviderType,
|
||||
)
|
||||
|
||||
|
||||
class _StubActorService:
|
||||
"""Stub actor service for testing."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.actors: dict[str, dict] = {}
|
||||
self.default_actor_name: str | None = None
|
||||
|
||||
def upsert_actor(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
provider: str,
|
||||
model: str,
|
||||
config_blob: dict,
|
||||
graph_descriptor: dict | None,
|
||||
unsafe: bool,
|
||||
set_default: bool,
|
||||
is_built_in: bool,
|
||||
yaml_text: str | None = None,
|
||||
schema_version: str | None = None,
|
||||
compiled_metadata: dict | None = None,
|
||||
) -> None:
|
||||
"""Store actor data for verification."""
|
||||
self.actors[name] = {
|
||||
"name": name,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"config_blob": config_blob,
|
||||
"graph_descriptor": graph_descriptor,
|
||||
"unsafe": unsafe,
|
||||
"is_built_in": is_built_in,
|
||||
"yaml_text": yaml_text,
|
||||
"schema_version": schema_version,
|
||||
}
|
||||
if set_default:
|
||||
self.default_actor_name = name
|
||||
|
||||
def get_default_actor(self) -> str | None:
|
||||
"""Return default actor name."""
|
||||
return self.default_actor_name
|
||||
|
||||
def set_default_actor(self, name: str) -> None:
|
||||
"""Set default actor."""
|
||||
self.default_actor_name = name
|
||||
|
||||
|
||||
class _StubProviderRegistry:
|
||||
"""Stub provider registry for testing."""
|
||||
|
||||
def __init__(self, providers: list[ProviderInfo]) -> None:
|
||||
self._providers = providers
|
||||
|
||||
def get_configured_providers(self) -> list[ProviderInfo]:
|
||||
"""Return configured providers."""
|
||||
return list(self._providers)
|
||||
|
||||
|
||||
class _StubSettings:
|
||||
"""Stub settings for testing."""
|
||||
|
||||
def __init__(self, defaults: ProviderDefaults) -> None:
|
||||
self._defaults = defaults
|
||||
|
||||
def resolve_provider_defaults(self) -> ProviderDefaults:
|
||||
"""Return provider defaults."""
|
||||
return self._defaults
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stub_actor_service() -> _StubActorService:
|
||||
"""Create a stub actor service."""
|
||||
return _StubActorService()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stub_settings() -> _StubSettings:
|
||||
"""Create stub settings."""
|
||||
return _StubSettings(
|
||||
ProviderDefaults(
|
||||
provider=None, provider_source="test", model=None, model_source="test"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestGenerateBuiltinActorYaml:
|
||||
"""Tests for _generate_builtin_actor_yaml method."""
|
||||
|
||||
def test_generates_yaml_with_type_field(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""YAML should include type: llm field."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
assert yaml_text is not None
|
||||
assert "type: llm" in yaml_text
|
||||
|
||||
def test_generates_yaml_with_description_field(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""YAML should include description field."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
assert "description:" in yaml_text
|
||||
assert "Built-in actor from provider registry" in yaml_text
|
||||
|
||||
def test_generates_yaml_with_name_field(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""YAML should include properly formatted name field."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
assert "name: openai/gpt-4" in yaml_text
|
||||
|
||||
def test_generates_yaml_with_provider_field(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""YAML should include provider field."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="anthropic",
|
||||
model="claude-3-opus",
|
||||
)
|
||||
|
||||
assert "provider: anthropic" in yaml_text
|
||||
|
||||
def test_generates_yaml_with_model_field(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""YAML should include model field."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
assert "model: openai/gpt-4" in yaml_text
|
||||
|
||||
def test_generates_yaml_with_source_field(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""YAML should include source: provider-registry field."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
assert "source: provider-registry" in yaml_text
|
||||
|
||||
def test_generated_yaml_is_valid_v3_format(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""Generated YAML should be recognized as v3 format."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
config_blob = yaml.safe_load(yaml_text)
|
||||
assert is_v3_yaml(config_blob), (
|
||||
"Generated YAML should be recognized as v3 format"
|
||||
)
|
||||
|
||||
def test_generated_yaml_validates_against_schema(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""Generated YAML should validate against ActorConfigSchema."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
config_blob = yaml.safe_load(yaml_text)
|
||||
schema = ActorConfigSchema.model_validate(config_blob)
|
||||
assert schema.type == ActorType.LLM
|
||||
assert schema.name == "openai/gpt-4"
|
||||
assert schema.provider == "openai"
|
||||
assert schema.model == "openai/gpt-4"
|
||||
|
||||
def test_generated_yaml_includes_capabilities(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""Generated YAML should include capabilities when provided."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
capabilities = ProviderCapabilities(supports_tools=True, supports_vision=False)
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openai",
|
||||
model="gpt-4",
|
||||
capabilities=capabilities,
|
||||
)
|
||||
|
||||
assert "capabilities:" in yaml_text
|
||||
assert "supports_tools: true" in yaml_text
|
||||
|
||||
def test_generated_yaml_sanitizes_slashes_in_name(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""Generated YAML name should sanitize slashes."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
yaml_text = registry._generate_builtin_actor_yaml(
|
||||
provider="openrouter",
|
||||
model="meta-llama/llama-3-70b-instruct",
|
||||
)
|
||||
|
||||
# Name should have slashes replaced with dashes
|
||||
assert "name: openrouter/meta-llama-llama-3-70b-instruct" in yaml_text
|
||||
|
||||
|
||||
class TestEnsureBuiltInActorsWithYaml:
|
||||
"""Tests for ensure_built_in_actors with yaml_text generation."""
|
||||
|
||||
def test_ensure_built_in_actors_includes_yaml_text(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""Built-in actors should have yaml_text populated."""
|
||||
provider_registry = _StubProviderRegistry(
|
||||
[
|
||||
ProviderInfo(
|
||||
provider_type=ProviderType.OPENAI,
|
||||
name="openai",
|
||||
api_key_env_var="ENV",
|
||||
default_model="gpt-4",
|
||||
capabilities=ProviderCapabilities(supports_tools=True),
|
||||
is_configured=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
registry.ensure_built_in_actors()
|
||||
|
||||
assert "openai/gpt-4" in stub_actor_service.actors
|
||||
actor_data = stub_actor_service.actors["openai/gpt-4"]
|
||||
assert actor_data["yaml_text"] is not None
|
||||
assert "type: llm" in actor_data["yaml_text"]
|
||||
|
||||
def test_ensure_built_in_actors_yaml_is_v3_format(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""Built-in actor yaml_text should be v3 format."""
|
||||
provider_registry = _StubProviderRegistry(
|
||||
[
|
||||
ProviderInfo(
|
||||
provider_type=ProviderType.ANTHROPIC,
|
||||
name="anthropic",
|
||||
api_key_env_var="ENV",
|
||||
default_model="claude-3-opus",
|
||||
capabilities=ProviderCapabilities(),
|
||||
is_configured=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
registry.ensure_built_in_actors()
|
||||
|
||||
actor_data = stub_actor_service.actors["anthropic/claude-3-opus"]
|
||||
assert actor_data["yaml_text"] is not None
|
||||
config_blob = yaml.safe_load(actor_data["yaml_text"])
|
||||
assert is_v3_yaml(config_blob)
|
||||
|
||||
def test_ensure_built_in_actors_multiple_providers(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""Multiple providers should each get yaml_text."""
|
||||
provider_registry = _StubProviderRegistry(
|
||||
[
|
||||
ProviderInfo(
|
||||
provider_type=ProviderType.OPENAI,
|
||||
name="openai",
|
||||
api_key_env_var="ENV",
|
||||
default_model="gpt-4",
|
||||
capabilities=ProviderCapabilities(supports_tools=True),
|
||||
is_configured=True,
|
||||
),
|
||||
ProviderInfo(
|
||||
provider_type=ProviderType.ANTHROPIC,
|
||||
name="anthropic",
|
||||
api_key_env_var="ENV",
|
||||
default_model="claude-3-opus",
|
||||
capabilities=ProviderCapabilities(),
|
||||
is_configured=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
registry.ensure_built_in_actors()
|
||||
|
||||
assert len(stub_actor_service.actors) == 2
|
||||
for actor_data in stub_actor_service.actors.values():
|
||||
assert actor_data["yaml_text"] is not None
|
||||
assert "type: llm" in actor_data["yaml_text"]
|
||||
|
||||
def test_ensure_built_in_actors_empty_when_no_providers(
|
||||
self, stub_actor_service: _StubActorService, stub_settings: _StubSettings
|
||||
) -> None:
|
||||
"""No providers should result in no actors."""
|
||||
provider_registry = _StubProviderRegistry([])
|
||||
registry = ActorRegistry(
|
||||
actor_service=stub_actor_service,
|
||||
provider_registry=provider_registry,
|
||||
settings=stub_settings,
|
||||
)
|
||||
|
||||
actors = registry.ensure_built_in_actors()
|
||||
|
||||
assert actors == []
|
||||
assert len(stub_actor_service.actors) == 0
|
||||
Reference in New Issue
Block a user