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

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:
2026-04-28 08:53:49 +00:00
committed by Forgejo
parent 9888c2f6e6
commit bb6765d85e
5 changed files with 688 additions and 1 deletions
+414
View File
@@ -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