338 lines
11 KiB
Python
338 lines
11 KiB
Python
"""Behave steps covering actor registry behaviors."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.actor.registry import ActorRegistry
|
|
from cleveragents.config.settings import ProviderDefaults
|
|
from cleveragents.core.exceptions import ValidationError
|
|
from cleveragents.domain.models.core.actor import Actor
|
|
from cleveragents.providers.registry import (
|
|
ProviderCapabilities,
|
|
ProviderInfo,
|
|
ProviderType,
|
|
)
|
|
|
|
|
|
class _StubSettings:
|
|
def __init__(self, defaults: ProviderDefaults) -> None:
|
|
self._defaults = defaults
|
|
|
|
def resolve_provider_defaults(self) -> ProviderDefaults:
|
|
return self._defaults
|
|
|
|
|
|
class _StubProviderRegistry:
|
|
def __init__(self, providers: list[ProviderInfo]) -> None:
|
|
self._providers = providers
|
|
self.calls = 0
|
|
|
|
def get_configured_providers(self) -> list[ProviderInfo]:
|
|
self.calls += 1
|
|
return list(self._providers)
|
|
|
|
|
|
class _StubActorService:
|
|
def __init__(self) -> None:
|
|
self.actors: dict[str, Actor] = {}
|
|
self.default_actor_name: str | None = None
|
|
self.upsert_payloads: list[dict[str, Any]] = []
|
|
|
|
def upsert_actor(
|
|
self,
|
|
*,
|
|
name: str,
|
|
provider: str,
|
|
model: str,
|
|
config_blob: dict[str, Any],
|
|
graph_descriptor: dict[str, Any] | None,
|
|
unsafe: bool,
|
|
set_default: bool,
|
|
is_built_in: bool,
|
|
yaml_text: str | None = None,
|
|
schema_version: str | None = None,
|
|
compiled_metadata: dict[str, Any] | None = None,
|
|
) -> Actor:
|
|
actor = Actor(
|
|
id=None,
|
|
name=name,
|
|
provider=provider,
|
|
model=model,
|
|
config_blob=config_blob,
|
|
config_hash=Actor.compute_hash(config_blob),
|
|
graph_descriptor=graph_descriptor,
|
|
yaml_text=yaml_text,
|
|
schema_version=schema_version or "1.0",
|
|
compiled_metadata=compiled_metadata,
|
|
unsafe=unsafe,
|
|
is_built_in=is_built_in,
|
|
is_default=False,
|
|
)
|
|
self.actors[name] = actor
|
|
if set_default:
|
|
self.set_default_actor(name)
|
|
self.upsert_payloads.append(
|
|
{
|
|
"name": name,
|
|
"provider": provider,
|
|
"model": model,
|
|
"config_blob": dict(config_blob),
|
|
"graph_descriptor": graph_descriptor,
|
|
"unsafe": unsafe,
|
|
"set_default": set_default,
|
|
"is_built_in": is_built_in,
|
|
}
|
|
)
|
|
return actor
|
|
|
|
def get_default_actor(self) -> Actor | None:
|
|
if self.default_actor_name and self.default_actor_name in self.actors:
|
|
return self.actors[self.default_actor_name]
|
|
return None
|
|
|
|
def set_default_actor(self, name: str) -> Actor:
|
|
actor = self.actors.get(name)
|
|
if actor is None:
|
|
raise ValueError("Actor does not exist")
|
|
self.default_actor_name = name
|
|
for value in self.actors.values():
|
|
value.is_default = value.name == name
|
|
return actor
|
|
|
|
def get_actor(self, name: str) -> Actor | None:
|
|
return self.actors.get(name)
|
|
|
|
def list_actors(self) -> list[Actor]:
|
|
return list(self.actors.values())
|
|
|
|
def remove_actor(self, name: str) -> None:
|
|
self.actors.pop(name, None)
|
|
if self.default_actor_name == name:
|
|
self.default_actor_name = None
|
|
|
|
|
|
def _default_defaults() -> ProviderDefaults:
|
|
return ProviderDefaults(
|
|
provider=None, provider_source="test", model=None, model_source="test"
|
|
)
|
|
|
|
|
|
def _build_registry(
|
|
context: Context,
|
|
providers: list[ProviderInfo],
|
|
defaults: ProviderDefaults | None = None,
|
|
) -> None:
|
|
context.actor_service = _StubActorService()
|
|
context.provider_registry = _StubProviderRegistry(providers)
|
|
context.settings = _StubSettings(defaults or _default_defaults())
|
|
context.registry = ActorRegistry(
|
|
actor_service=context.actor_service,
|
|
provider_registry=context.provider_registry,
|
|
settings=context.settings,
|
|
)
|
|
|
|
|
|
@given('provider defaults are provider "{provider}" model "{model}"')
|
|
def step_provider_defaults(context: Context, provider: str, model: str) -> None:
|
|
normalized_provider = provider or None
|
|
normalized_model = model or None
|
|
context.default_values = ProviderDefaults(
|
|
provider=normalized_provider,
|
|
provider_source="scenario",
|
|
model=normalized_model,
|
|
model_source="scenario",
|
|
)
|
|
|
|
|
|
@given("an actor registry with configured providers:")
|
|
def step_registry_with_providers(context: Context) -> None:
|
|
providers: list[ProviderInfo] = []
|
|
for row in context.table:
|
|
provider_type = ProviderType(row["type"])
|
|
providers.append(
|
|
ProviderInfo(
|
|
provider_type=provider_type,
|
|
name=row["name"],
|
|
api_key_env_var="ENV",
|
|
default_model=row["model"],
|
|
capabilities=ProviderCapabilities(),
|
|
is_configured=True,
|
|
)
|
|
)
|
|
defaults = getattr(context, "default_values", _default_defaults())
|
|
_build_registry(context, providers, defaults)
|
|
|
|
|
|
@given("an actor registry with no configured providers")
|
|
def step_registry_with_no_providers(context: Context) -> None:
|
|
defaults = getattr(context, "default_values", _default_defaults())
|
|
_build_registry(context, [], defaults)
|
|
|
|
|
|
@when("I ensure built-in actors are generated")
|
|
def step_ensure_built_ins(context: Context) -> None:
|
|
context.generated_actors = context.registry.ensure_built_in_actors()
|
|
|
|
|
|
@then("no actors should be created")
|
|
def step_no_actors_created(context: Context) -> None:
|
|
assert not context.actor_service.actors
|
|
assert context.generated_actors == []
|
|
|
|
|
|
@then("no default actor should be set")
|
|
def step_no_default_set(context: Context) -> None:
|
|
assert context.actor_service.get_default_actor() is None
|
|
|
|
|
|
@then("the registry should list actors {expected}")
|
|
def step_registry_should_list(context: Context, expected: str) -> None:
|
|
expected_list = ast.literal_eval(expected)
|
|
actual_names = [actor.name for actor in context.registry.list_actors()]
|
|
assert actual_names == expected_list, actual_names
|
|
|
|
|
|
@then('the default actor should be "{expected}"')
|
|
def step_default_actor(context: Context, expected: str) -> None:
|
|
actor = context.actor_service.get_default_actor()
|
|
assert actor is not None, "No default actor was set"
|
|
assert actor.name == expected
|
|
|
|
|
|
@when('I remove actor "{name}"')
|
|
def step_remove_actor(context: Context, name: str) -> None:
|
|
context.registry.remove_actor(name)
|
|
|
|
|
|
@then("the stored actors should be {expected}")
|
|
def step_stored_actors(context: Context, expected: str) -> None:
|
|
expected_list = ast.literal_eval(expected)
|
|
assert sorted(context.actor_service.actors.keys()) == sorted(expected_list)
|
|
|
|
|
|
@when('I attempt to upsert actor "{name}" with unsafe config')
|
|
def step_upsert_unsafe_actor(context: Context, name: str) -> None:
|
|
try:
|
|
context.registry.upsert_actor(
|
|
name=name,
|
|
config_blob={"provider": "local", "model": "unsafe", "unsafe": True},
|
|
)
|
|
context.error = None
|
|
except Exception as exc:
|
|
context.error = exc
|
|
|
|
|
|
@then("a registry validation error should be raised for unsafe actor")
|
|
def step_expect_validation_error(context: Context) -> None:
|
|
assert isinstance(context.error, ValidationError), type(context.error)
|
|
|
|
|
|
@when(
|
|
'I upsert actor "{name}" with provider "{provider}" model "{model}" graph descriptor {graph_descriptor} and options {options_blob}'
|
|
)
|
|
def step_upsert_actor_with_graph(
|
|
context: Context,
|
|
name: str,
|
|
provider: str,
|
|
model: str,
|
|
graph_descriptor: str,
|
|
options_blob: str,
|
|
) -> None:
|
|
graph = ast.literal_eval(graph_descriptor)
|
|
options = ast.literal_eval(options_blob)
|
|
context.saved_actor = context.registry.upsert_actor(
|
|
name=name,
|
|
provider=provider,
|
|
model=model,
|
|
graph_descriptor=graph,
|
|
config_blob=options,
|
|
)
|
|
|
|
|
|
@when('I upsert actor "{name}" with raw config {config_blob}')
|
|
def step_upsert_actor_raw_config(context: Context, name: str, config_blob: str) -> None:
|
|
blob = ast.literal_eval(config_blob)
|
|
context.saved_actor = context.registry.upsert_actor(name=name, config_blob=blob)
|
|
|
|
|
|
@when('I retrieve actor "{name}" via the registry')
|
|
def step_get_actor_via_registry(context: Context, name: str) -> None:
|
|
context.retrieved_actor = context.registry.get_actor(name)
|
|
|
|
|
|
@when('I set the default actor to "{name}" via the registry')
|
|
def step_set_default_actor_via_registry(context: Context, name: str) -> None:
|
|
context.registry.set_default_actor(name)
|
|
|
|
|
|
@then(
|
|
'the stored actor config should include provider "{provider}" model "{model}" graph descriptor {graph_descriptor} unsafe false and option "{option_key}" "{option_value}"'
|
|
)
|
|
def step_assert_canonical_blob(
|
|
context: Context,
|
|
provider: str,
|
|
model: str,
|
|
graph_descriptor: str,
|
|
option_key: str,
|
|
option_value: str,
|
|
) -> None:
|
|
graph = ast.literal_eval(graph_descriptor)
|
|
assert context.actor_service.upsert_payloads, "No actor was upserted"
|
|
blob = context.actor_service.upsert_payloads[-1]["config_blob"]
|
|
assert blob["provider"] == provider
|
|
assert blob["model"] == model
|
|
assert blob["graph_descriptor"] == graph
|
|
assert blob.get("unsafe") is False
|
|
assert blob.get(option_key) == option_value
|
|
|
|
|
|
@then(
|
|
'the saved actor config should keep graph descriptor {graph_descriptor} and option "{option_key}" {option_value}'
|
|
)
|
|
def step_assert_preserved_graph_and_option(
|
|
context: Context,
|
|
graph_descriptor: str,
|
|
option_key: str,
|
|
option_value: str,
|
|
) -> None:
|
|
graph = ast.literal_eval(graph_descriptor)
|
|
parsed_value = ast.literal_eval(option_value)
|
|
assert context.actor_service.upsert_payloads, "No actor was upserted"
|
|
blob = context.actor_service.upsert_payloads[-1]["config_blob"]
|
|
assert blob["graph_descriptor"] == graph
|
|
assert blob.get("options", {}).get(option_key) == parsed_value
|
|
assert blob.get("unsafe") is False
|
|
|
|
|
|
@then('the registry default actor via wrapper should be "{expected}"')
|
|
def step_assert_wrapper_default_actor(context: Context, expected: str) -> None:
|
|
actor = context.registry.get_default_actor()
|
|
assert actor is not None, "No default actor set via registry"
|
|
assert actor.name == expected
|
|
|
|
|
|
@then(
|
|
'the built-in actor payload should include graph descriptor and capabilities for "{actor_name}"'
|
|
)
|
|
def step_assert_built_in_payload(context: Context, actor_name: str) -> None:
|
|
assert context.actor_service.upsert_payloads, "No actors were persisted"
|
|
payload = None
|
|
for item in context.actor_service.upsert_payloads:
|
|
if item.get("name") == actor_name:
|
|
payload = item
|
|
break
|
|
assert payload is not None, f"Actor {actor_name} not found in payloads"
|
|
blob = payload["config_blob"]
|
|
graph = blob.get("graph_descriptor")
|
|
assert graph is not None, "graph_descriptor missing on built-in actor"
|
|
assert graph.get("provider")
|
|
assert graph.get("model")
|
|
assert graph.get("source") == "provider-registry"
|
|
capabilities = blob.get("capabilities")
|
|
assert capabilities is not None, "capabilities missing on built-in actor config"
|