"""Steps for SimpleToolAgent and SimpleLLMAgent coverage.""" from __future__ import annotations from typing import Any from behave import given, then, when from behave.runner import Context import cleveragents.reactive.stream_router as stream_router from cleveragents.core.exceptions import StreamRoutingError from cleveragents.reactive.stream_router import SimpleLLMAgent, SimpleToolAgent class _WorkingTemplate: def __init__(self, template: str) -> None: self.template = template def render(self, **kwargs: Any) -> str: name = kwargs.get("name") if name is not None: return self.template.replace("{{ name }}", str(name)) return f"rendered:{self.template}" class _WorkingTemplateEnv: def from_string(self, template: str) -> _WorkingTemplate: return _WorkingTemplate(template) class _FailingTemplate: def render(self, **_kwargs: Any) -> str: raise RuntimeError("render failed") class _FailingTemplateEnv: def from_string(self, _template: str) -> _FailingTemplate: return _FailingTemplate() class _MessageStub: def __init__(self, content: str) -> None: self.content = content class _LLMStub: def __init__(self, response_content: str = "llm-output") -> None: self.response_content = response_content self.invocations: list[list[Any]] = [] def invoke(self, messages: list[Any]) -> Any: self.invocations.append(messages) class _Response: def __init__(self, content: str) -> None: self.content = content return _Response(self.response_content) class _RegistryStub: def __init__(self, llm: _LLMStub) -> None: self.llm = llm self.calls: list[dict[str, Any]] = [] def create_llm(self, **kwargs: Any) -> _LLMStub: self.calls.append(kwargs) return self.llm def _register_cleanup(context: Context, cleanup_fn) -> None: cleanup = getattr(context, "add_cleanup", None) if callable(cleanup): cleanup(cleanup_fn) @given("a SimpleToolAgent with a tool missing code") def step_tool_agent_missing_code(context: Context) -> None: context.tool_agent = SimpleToolAgent(tools=[{}]) @given("a SimpleToolAgent with a code block tool") def step_tool_agent_code_block(context: Context) -> None: context.tool_agent = SimpleToolAgent( tools=[{"code": "result = 'ok:' + str(input_data)"}] ) @when('I process "{content}" through the SimpleToolAgent') def step_process_tool_agent(context: Context, content: str) -> None: context.tool_result = context.tool_agent.process(content) @when('I process "{content}" through the SimpleToolAgent expecting rejection') def step_process_tool_agent_reject(context: Context, content: str) -> None: context.tool_error = None try: context.tool_result = context.tool_agent.process(content) except Exception as exc: # pylint: disable=broad-except context.tool_error = exc @when('I call process_message_sync with "{content}"') def step_process_tool_agent_sync(context: Context, content: str) -> None: context.tool_result = context.tool_agent.process_message_sync(content) @when('I call process_message_sync with "{content}" expecting rejection') def step_process_tool_agent_sync_reject(context: Context, content: str) -> None: context.tool_error = None try: context.tool_result = context.tool_agent.process_message_sync(content) except Exception as exc: # pylint: disable=broad-except context.tool_error = exc @then('the SimpleToolAgent result should be "{expected}"') def step_tool_agent_result(context: Context, expected: str) -> None: assert context.tool_result == expected @then("the SimpleToolAgent result should be empty") def step_tool_agent_result_empty(context: Context) -> None: assert context.tool_result == "" @then("the SimpleToolAgent should raise a routing error about code blocks") def step_tool_agent_code_block_rejected(context: Context) -> None: assert isinstance(context.tool_error, StreamRoutingError), ( f"Expected StreamRoutingError, got {type(context.tool_error).__name__}: " f"{context.tool_error}" ) assert "code" in str(context.tool_error).lower() assert "not supported" in str(context.tool_error).lower() @given("a SimpleLLMAgent for prompt rendering") def step_llm_agent_for_rendering(context: Context) -> None: context.llm_agent = SimpleLLMAgent("renderer", {"system_prompt": ""}) @given('the SimpleLLMAgent template environment is "{mode}"') def step_set_template_env(context: Context, mode: str) -> None: if mode == "none": context.llm_agent._template_env = None return if mode == "working": context.llm_agent._template_env = _WorkingTemplateEnv() return if mode == "failing": context.llm_agent._template_env = _FailingTemplateEnv() return raise ValueError(f"Unknown template env mode: {mode}") @when('I render the template "{template}" with context:') def step_render_template(context: Context, template: str) -> None: table = getattr(context, "table", None) payload = {row["key"]: row["value"] for row in table} if table else {} if template == "": template = "" context.rendered_prompt = context.llm_agent._render_prompt(template, payload) @then('the rendered prompt should be "{expected}"') def step_rendered_prompt(context: Context, expected: str) -> None: if expected == "": expected = "" assert context.rendered_prompt == expected @given("a SimpleLLMAgent with provider config") def step_llm_agent_with_provider_config(context: Context) -> None: context.llm_agent = SimpleLLMAgent( "agent", { "provider": "stub", "model": "stub-model", "temperature": 0.7, "max_tokens": 50, "max_retries": 2, }, ) @given("a SimpleLLMAgent with system prompt and provider config") def step_llm_agent_with_system_prompt(context: Context) -> None: context.llm_agent = SimpleLLMAgent( "agent", { "provider": "stub", "model": "stub-model", "temperature": 0.7, "max_tokens": 50, "max_retries": 2, "system_prompt": "system", }, ) @given("a stub provider registry is installed") def step_stub_provider_registry(context: Context) -> None: context.llm_stub = _LLMStub() context.registry_stub = _RegistryStub(context.llm_stub) context.original_registry = stream_router.get_provider_registry stream_router.get_provider_registry = lambda: context.registry_stub _register_cleanup( context, lambda: setattr( stream_router, "get_provider_registry", context.original_registry ), ) @given("LangChain message classes are unavailable") def step_langchain_unavailable(context: Context) -> None: context.original_human = stream_router.HumanMessage context.original_system = stream_router.SystemMessage stream_router.HumanMessage = None stream_router.SystemMessage = None _register_cleanup( context, lambda: ( setattr(stream_router, "HumanMessage", context.original_human), setattr(stream_router, "SystemMessage", context.original_system), ), ) @given("LangChain message stubs are installed") def step_langchain_stubbed(context: Context) -> None: context.original_human = stream_router.HumanMessage context.original_system = stream_router.SystemMessage stream_router.HumanMessage = _MessageStub stream_router.SystemMessage = _MessageStub _register_cleanup( context, lambda: ( setattr(stream_router, "HumanMessage", context.original_human), setattr(stream_router, "SystemMessage", context.original_system), ), ) @when("I resolve the LLM twice") def step_resolve_llm_twice(context: Context) -> None: context.llm_first = context.llm_agent._resolve_llm() context.llm_second = context.llm_agent._resolve_llm() @then("the registry should be called once with the configured kwargs") def step_verify_registry_calls(context: Context) -> None: assert len(context.registry_stub.calls) == 1 call = context.registry_stub.calls[0] assert call.get("provider_type") == "stub" assert call.get("model_id") == "stub-model" assert call.get("temperature") == 0.7 assert call.get("max_tokens") == 50 assert call.get("max_retries") == 2 @then("the resolved LLM should be cached") def step_verify_llm_cached(context: Context) -> None: assert context.llm_first is context.llm_second @when('I process "{content}" through the LLM agent') def step_process_llm_agent(context: Context, content: str) -> None: context.process_error = None try: context.process_result = context.llm_agent.process(content) except Exception as exc: # pylint: disable=broad-except context.process_error = exc @then("I should receive a stream routing error about missing messages") def step_verify_missing_messages_error(context: Context) -> None: assert isinstance(context.process_error, StreamRoutingError) assert "LangChain messages not available" in str(context.process_error) @when("I process 123 through the LLM agent via process_message_sync") def step_process_llm_agent_sync(context: Context) -> None: context.process_result = context.llm_agent.process_message_sync(123) @then("the LLM should receive system and human messages") def step_verify_llm_messages(context: Context) -> None: assert context.llm_stub.invocations messages = context.llm_stub.invocations[0] assert len(messages) == 2 assert isinstance(messages[0], _MessageStub) assert isinstance(messages[1], _MessageStub) # System prompt is augmented with boundary instructions (mechanism 2) assert "system" in messages[0].content # User content is wrapped with boundary markers (mechanism 2) assert "123" in messages[1].content @then('the LLM agent result should be "llm-output"') def step_verify_llm_result(context: Context) -> None: assert context.process_result == "llm-output"