fix(actor): Get responses from actor-run
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 37s
CI / helm (pull_request) Successful in 42s
CI / build (pull_request) Successful in 1m8s
CI / quality (pull_request) Successful in 1m32s
CI / lint (pull_request) Failing after 1m37s
CI / typecheck (pull_request) Successful in 1m52s
CI / security (pull_request) Successful in 2m5s
CI / coverage (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 4m23s
CI / e2e_tests (pull_request) Successful in 4m45s
CI / unit_tests (pull_request) Successful in 8m16s
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 37s
CI / helm (pull_request) Successful in 42s
CI / build (pull_request) Successful in 1m8s
CI / quality (pull_request) Successful in 1m32s
CI / lint (pull_request) Failing after 1m37s
CI / typecheck (pull_request) Successful in 1m52s
CI / security (pull_request) Successful in 2m5s
CI / coverage (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 4m23s
CI / e2e_tests (pull_request) Successful in 4m45s
CI / unit_tests (pull_request) Successful in 8m16s
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
Built-in actors generated from the provider registry have a config_blob with provider and model fields but no type field. When this raw blob is serialised to YAML and fed to ReactiveConfigParser, the parser produces an empty ReactiveConfig with no agents and no routes, causing run_single_shot() to return empty string (bug #10861). Fix: resolve_config_files now synthesises a minimal v3 type: llm YAML when the actor has no yaml_text and the config_blob has provider and model but no type field. This allows the reactive config parser to create a working agent and graph route, enabling the LLM to be invoked and its response returned to the caller. ISSUES CLOSED: #10861
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""Step definitions for TDD Bug #10861 - agents actor run returns nothing.
|
||||
|
||||
These steps verify that resolve_config_files synthesises a v3 type: llm YAML
|
||||
when the actor has a built-in config_blob with provider and model but no type
|
||||
field. This is the regression guard for bug #10861.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when # type: ignore[import-untyped]
|
||||
from behave.runner import Context # type: ignore[import-untyped]
|
||||
|
||||
from cleveragents.cli.commands._resolve_actor import resolve_config_files
|
||||
|
||||
|
||||
@given("a built-in actor with provider and model but no type field for tdd-10861")
|
||||
def step_given_builtin_actor(context: Context) -> None:
|
||||
"""Set up a mock built-in actor with provider/model but no type field."""
|
||||
mock_actor = MagicMock()
|
||||
mock_actor.name = "anthropic/claude-sonnet-4-20250514"
|
||||
mock_actor.yaml_text = ""
|
||||
mock_actor.config_blob = {
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"capabilities": {},
|
||||
"unsafe": False,
|
||||
"source": "provider-registry",
|
||||
}
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = mock_actor
|
||||
mock_container = MagicMock()
|
||||
mock_container.actor_registry.return_value = mock_registry
|
||||
context.mock_actor = mock_actor
|
||||
context.mock_container = mock_container
|
||||
|
||||
|
||||
@given("an actor with existing yaml_text for tdd-10861")
|
||||
def step_given_actor_with_yaml_text(context: Context) -> None:
|
||||
"""Set up a mock actor that already has yaml_text."""
|
||||
mock_actor = MagicMock()
|
||||
mock_actor.name = "local/my-custom-actor"
|
||||
mock_actor.yaml_text = "name: local/my-custom-actor\ntype: llm\nprovider: openai\nmodel: gpt-4\n"
|
||||
mock_actor.config_blob = None
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = mock_actor
|
||||
mock_container = MagicMock()
|
||||
mock_container.actor_registry.return_value = mock_registry
|
||||
context.mock_actor = mock_actor
|
||||
context.mock_container = mock_container
|
||||
|
||||
|
||||
@when("resolve_config_files is called with the built-in actor name for tdd-10861")
|
||||
def step_when_resolve_builtin(context: Context) -> None:
|
||||
"""Call resolve_config_files with the built-in actor name."""
|
||||
with patch(
|
||||
"cleveragents.cli.commands._resolve_actor.get_container",
|
||||
return_value=context.mock_container,
|
||||
):
|
||||
context.result_paths = resolve_config_files(
|
||||
"anthropic/claude-sonnet-4-20250514", []
|
||||
)
|
||||
context.add_cleanup(
|
||||
lambda: [p.unlink(missing_ok=True) for p in context.result_paths]
|
||||
)
|
||||
|
||||
|
||||
@when("resolve_config_files is called with the actor name for tdd-10861")
|
||||
def step_when_resolve_actor(context: Context) -> None:
|
||||
"""Call resolve_config_files with the actor name."""
|
||||
with patch(
|
||||
"cleveragents.cli.commands._resolve_actor.get_container",
|
||||
return_value=context.mock_container,
|
||||
):
|
||||
context.result_paths = resolve_config_files("local/my-custom-actor", [])
|
||||
context.add_cleanup(
|
||||
lambda: [p.unlink(missing_ok=True) for p in context.result_paths]
|
||||
)
|
||||
|
||||
|
||||
@then("the resulting YAML file contains type llm for tdd-10861")
|
||||
def step_then_yaml_contains_type_llm(context: Context) -> None:
|
||||
"""Assert the synthesised YAML contains type: llm."""
|
||||
assert len(context.result_paths) == 1
|
||||
tmp_path: Path = context.result_paths[0]
|
||||
assert tmp_path.exists(), f"Temp file does not exist: {tmp_path}"
|
||||
content = tmp_path.read_text(encoding="utf-8")
|
||||
parsed = yaml.safe_load(content)
|
||||
assert isinstance(parsed, dict), f"Expected dict, got {type(parsed)}"
|
||||
assert parsed.get("type") == "llm", (
|
||||
"Expected type: llm in synthesised YAML, got: "
|
||||
+ str(parsed.get("type"))
|
||||
+ "\nFull content:\n"
|
||||
+ content
|
||||
)
|
||||
|
||||
|
||||
@then("the resulting YAML file contains the provider and model for tdd-10861")
|
||||
def step_then_yaml_contains_provider_model(context: Context) -> None:
|
||||
"""Assert the synthesised YAML contains the correct provider and model."""
|
||||
tmp_path: Path = context.result_paths[0]
|
||||
content = tmp_path.read_text(encoding="utf-8")
|
||||
parsed = yaml.safe_load(content)
|
||||
assert parsed.get("provider") == "anthropic", (
|
||||
"Expected provider: anthropic, got: " + str(parsed.get("provider"))
|
||||
)
|
||||
assert parsed.get("model") == "claude-sonnet-4-20250514", (
|
||||
"Expected model: claude-sonnet-4-20250514, got: " + str(parsed.get("model"))
|
||||
)
|
||||
|
||||
|
||||
@then("the resulting YAML file is parseable as a v3 llm actor config for tdd-10861")
|
||||
def step_then_yaml_parseable_as_v3(context: Context) -> None:
|
||||
"""Assert the synthesised YAML is parseable by ReactiveConfigParser."""
|
||||
from cleveragents.reactive.config_parser import ReactiveConfigParser
|
||||
|
||||
tmp_path: Path = context.result_paths[0]
|
||||
parser = ReactiveConfigParser()
|
||||
rc = parser.parse_files([tmp_path])
|
||||
|
||||
assert rc.agents, (
|
||||
"ReactiveConfigParser produced no agents from synthesised YAML. "
|
||||
"This means run_single_shot() would return empty string (bug #10861)."
|
||||
)
|
||||
assert rc.routes, (
|
||||
"ReactiveConfigParser produced no routes from synthesised YAML. "
|
||||
"This means run_single_shot() would return empty string (bug #10861)."
|
||||
)
|
||||
|
||||
|
||||
@then("the original yaml_text is used without modification for tdd-10861")
|
||||
def step_then_original_yaml_used(context: Context) -> None:
|
||||
"""Assert that actors with existing yaml_text are not affected by the fix."""
|
||||
tmp_path: Path = context.result_paths[0]
|
||||
assert tmp_path.exists(), f"Temp file does not exist: {tmp_path}"
|
||||
content = tmp_path.read_text(encoding="utf-8")
|
||||
assert "local/my-custom-actor" in content, (
|
||||
"Expected original yaml_text content, got: " + content
|
||||
)
|
||||
assert "type: llm" in content, (
|
||||
"Expected type: llm from original yaml_text, got: " + content
|
||||
)
|
||||
assert "provider: openai" in content, (
|
||||
"Expected provider: openai from original yaml_text, got: " + content
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
@tdd_issue @tdd_issue_10861
|
||||
Feature: TDD Bug #10861 - agents actor run returns nothing for built-in LLM actors
|
||||
|
||||
Bug #10861 reports that running agents actor run with a built-in LLM actor
|
||||
returns nothing instead of a response from the LLM.
|
||||
|
||||
Root cause: built-in actors have a config_blob with provider and model
|
||||
fields but no type field. When serialised to YAML and fed to
|
||||
ReactiveConfigParser, the parser produces an empty ReactiveConfig with no
|
||||
agents and no routes, causing run_single_shot() to return empty string.
|
||||
|
||||
Fix: resolve_config_files now synthesises a minimal v3 type: llm YAML
|
||||
when the actor has no yaml_text and the config_blob has provider and model
|
||||
but no type field.
|
||||
|
||||
Scenario: resolve_config_files synthesises v3 llm YAML for built-in actor
|
||||
Given a built-in actor with provider and model but no type field for tdd-10861
|
||||
When resolve_config_files is called with the built-in actor name for tdd-10861
|
||||
Then the resulting YAML file contains type llm for tdd-10861
|
||||
And the resulting YAML file contains the provider and model for tdd-10861
|
||||
|
||||
Scenario: synthesised YAML is parseable by ReactiveConfigParser for tdd-10861
|
||||
Given a built-in actor with provider and model but no type field for tdd-10861
|
||||
When resolve_config_files is called with the built-in actor name for tdd-10861
|
||||
Then the resulting YAML file is parseable as a v3 llm actor config for tdd-10861
|
||||
|
||||
Scenario: actor with existing yaml_text is not affected by the fix for tdd-10861
|
||||
Given an actor with existing yaml_text for tdd-10861
|
||||
When resolve_config_files is called with the actor name for tdd-10861
|
||||
Then the original yaml_text is used without modification for tdd-10861
|
||||
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
import atexit
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import typer
|
||||
import yaml
|
||||
@@ -52,6 +53,44 @@ def _sanitize_name(name: str) -> str:
|
||||
return "".join(c for c in name if c.isprintable())
|
||||
|
||||
|
||||
|
||||
|
||||
def _synthesize_llm_yaml(actor_name: str, config_blob: dict[str, Any]) -> str:
|
||||
"""Synthesize a v3 type: llm YAML from a built-in actor config blob.
|
||||
|
||||
Built-in actors generated from the provider registry have a config_blob
|
||||
with provider and model fields but no type field. When this raw blob is
|
||||
serialised to YAML and fed to ReactiveConfigParser, the parser finds no
|
||||
type, no agents/actors map, and no routes key so it produces an empty
|
||||
ReactiveConfig with no agents and no routes. run_single_shot() then
|
||||
returns empty string because there is nothing to execute (fix #10861).
|
||||
|
||||
Args:
|
||||
actor_name: The namespaced actor name.
|
||||
config_blob: The actor canonical configuration blob from the registry.
|
||||
|
||||
Returns:
|
||||
A YAML string in v3 type: llm format that the reactive config
|
||||
parser can consume to produce a working single-agent graph route.
|
||||
"""
|
||||
provider = str(config_blob.get("provider") or "")
|
||||
model = str(config_blob.get("model") or "")
|
||||
|
||||
v3_blob: dict[str, Any] = {
|
||||
"name": actor_name,
|
||||
"type": "llm",
|
||||
"description": f"Built-in LLM actor for {provider}/{model}",
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
}
|
||||
|
||||
system_prompt = config_blob.get("system_prompt")
|
||||
if system_prompt:
|
||||
v3_blob["system_prompt"] = system_prompt
|
||||
|
||||
return yaml.safe_dump(v3_blob, default_flow_style=False, sort_keys=False)
|
||||
|
||||
|
||||
def resolve_config_files(name: str, config: list[Path]) -> list[Path]:
|
||||
"""Return config file paths, resolving *name* from the actor registry
|
||||
when *config* is empty.
|
||||
@@ -102,15 +141,28 @@ def resolve_config_files(name: str, config: list[Path]) -> list[Path]:
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(code=2)
|
||||
try:
|
||||
yaml_text = yaml.safe_dump(config_blob, default_flow_style=False)
|
||||
except yaml.YAMLError:
|
||||
typer.echo(
|
||||
f"Error: Actor '{safe_name}' config_blob could not be "
|
||||
"serialised to YAML.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(code=2) from None
|
||||
# Fix #10861: built-in actors have a config_blob with provider
|
||||
# and model but no type field. Serialising this blob as-is
|
||||
# produces YAML that the reactive config parser cannot interpret
|
||||
# (no agents, no routes -> empty ReactiveConfig -> empty response).
|
||||
# Synthesise a v3 type: llm YAML so the parser can create a
|
||||
# working agent and graph route.
|
||||
if (
|
||||
config_blob.get("provider")
|
||||
and config_blob.get("model")
|
||||
and not config_blob.get("type")
|
||||
):
|
||||
yaml_text = _synthesize_llm_yaml(actor.name, config_blob)
|
||||
else:
|
||||
try:
|
||||
yaml_text = yaml.safe_dump(config_blob, default_flow_style=False)
|
||||
except yaml.YAMLError:
|
||||
typer.echo(
|
||||
f"Error: Actor '{safe_name}' config_blob could not be "
|
||||
"serialised to YAML.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(code=2) from None
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
|
||||
|
||||
Reference in New Issue
Block a user