import asyncio from copy import deepcopy from behave import given, then, when from cleveragents.agents.base import Agent from cleveragents.langgraph.nodes import Node, NodeConfig, NodeType, ToolAgent from cleveragents.langgraph.state import GraphState def _make_state(messages=None, metadata=None): return GraphState(messages=messages or [], metadata=metadata or {}) class RecordingToolAgent(ToolAgent): def __init__(self, name: str, response: str): super().__init__(name) self.response = response self.received_inputs = [] async def process_message(self, message, context=None): self.received_inputs.append(message) return self.response def get_capabilities(self): return [] class RecordingAgent(Agent): def __init__(self, name: str, response: str): super().__init__(name) self.response = response self.received_inputs = [] self.received_contexts = [] async def process_message(self, message, context=None): self.received_inputs.append(message) if context is not None: self.received_contexts.append(deepcopy(context)) return self.response def get_capabilities(self): return [] class ClearingAgent(Agent): def __init__(self, name: str, response: str): super().__init__(name) self.response = response async def process_message(self, message, context=None): if context is not None: context.clear() return self.response def get_capabilities(self): return [] class MutatingAgent(Agent): def __init__(self, name: str, response: str): super().__init__(name) self.response = response async def process_message(self, message, context=None): if context is not None: context["mutated"] = "yes" return self.response def get_capabilities(self): return [] @given("a tool node configured") def step_tool_node_configured(context): config = NodeConfig(name="tool_node", type=NodeType.TOOL) context.node = Node(config) context.state = _make_state() @when("I execute the tool node") def step_execute_tool_node(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("it should mark tool executed") def step_assert_tool_executed(context): assert context.result.get("tool_executed") is True assert context.result.get("node") == "tool_node" @given('a tool agent node using ToolAgent with current_message "{text}"') def step_toolagent_branch(context, text): tool_agent = RecordingToolAgent("tool", response="ok") config = NodeConfig(name="tool_agent", type=NodeType.AGENT, agent="tool") context.node = Node(config, agents={"tool": tool_agent}) context.state = _make_state( messages=[{"role": "assistant", "content": "fallback"}], metadata={"current_message": text}, ) @when("I execute the tool agent node with ToolAgent branch") def step_execute_toolagent_branch(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then('the ToolAgent should receive input "{text}"') def step_assert_toolagent_input(context, text): agent = context.node.agents["tool"] assert agent.received_inputs[-1] == text @given("an agent node with only assistant history") def step_agent_assistant_only(context): agent = RecordingAgent("rec", response="ok") config = NodeConfig(name="assistant_only", type=NodeType.AGENT, agent="rec") context.node = Node(config, agents={"rec": agent}) context.state = _make_state(messages=[{"role": "assistant", "content": "assist"}]) @when("I execute the agent node for assistant fallback") def step_execute_agent_assistant_only(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("the agent should receive the assistant fallback content") def step_assert_assistant_fallback(context): agent = context.node.agents["rec"] assert agent.received_inputs[-1] == "assist" @given("an agent that clears context during processing") def step_agent_clears_context(context): agent = ClearingAgent("clear", response="done") config = NodeConfig(name="clear_agent", type=NodeType.AGENT, agent="clear") context.node = Node(config, agents={"clear": agent}) context.state = _make_state( messages=[{"role": "user", "content": "hi"}], metadata={"foo": "bar"} ) @when("I execute the clearing agent node") def step_execute_clearing_agent(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("only last_agent_node metadata should remain") def step_assert_cleared_metadata(context): metadata = context.result.get("metadata", {}) assert metadata == {"last_agent_node": "clear_agent"} @given("an agent that mutates context to add metadata") def step_agent_mutates_context(context): agent = MutatingAgent("mut", response="done") config = NodeConfig(name="mut_agent", type=NodeType.AGENT, agent="mut") context.node = Node(config, agents={"mut": agent}) context.state = _make_state(messages=[{"role": "user", "content": "hello"}]) @when("I execute the mutating agent node") def step_execute_mutating_agent(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("the metadata should include the mutated key") def step_assert_mutated_metadata(context): metadata = context.result.get("metadata", {}) assert metadata.get("mutated") == "yes" @given('a subgraph node named "{subgraph_name}"') def step_subgraph_node(context, subgraph_name): config = NodeConfig(name="subgraph", type=NodeType.SUBGRAPH, subgraph=subgraph_name) context.node = Node(config) context.state = _make_state() @when("I execute the subgraph node") def step_execute_subgraph(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then('it should report the invoked subgraph "{subgraph_name}"') def step_assert_subgraph(context, subgraph_name): assert context.result.get("subgraph_invoked") == subgraph_name @given("a start node") def step_start_node(context): config = NodeConfig(name="start", type=NodeType.START) context.node = Node(config) context.state = _make_state() @when("I execute the start node") def step_execute_start(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("it should indicate graph start") def step_assert_started(context): assert context.result.get("started") is True @given("an end node") def step_end_node(context): config = NodeConfig(name="end", type=NodeType.END) context.node = Node(config) context.state = _make_state() @when("I execute the end node") def step_execute_end(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("it should indicate graph completion") def step_assert_completed(context): assert context.result.get("completed") is True @given("a tool node patched to return non dict") def step_tool_node_patched(context): config = NodeConfig(name="patched_tool", type=NodeType.TOOL) context.node = Node(config) async def _patched_execute_tool(): return "raw-value" context.node._execute_tool = _patched_execute_tool # type: ignore[attr-defined] context.state = _make_state() @when("I execute the patched tool node") def step_execute_patched_tool(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("the execute result should only include current_node") def step_assert_current_node_only(context): assert context.result == {"current_node": "patched_tool"}