import asyncio from behave import given, then, when from cleveragents.agents.base import Agent from cleveragents.langgraph.nodes import Edge, Node, NodeConfig, NodeType from cleveragents.langgraph.state import GraphState class DummyAgent(Agent): 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 DummyToolAgent(Agent): 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 [] def _make_state(messages=None, metadata=None): return GraphState(messages=messages or [], metadata=metadata or {}) @given("a node with history limit 2 messages") def step_node_with_history_limit(context): config = NodeConfig( name="history", type=NodeType.AGENT, metadata={"max_history_messages": 2} ) context.node = Node(config) @given("a conversation history of 3 short messages") def step_history_three_messages(context): context.messages = [ {"role": "user", "content": "hi"}, {"role": "assistant", "content": "there"}, {"role": "user", "content": "friend"}, ] @when("I prepare the conversation history") def step_prepare_history(context): trimmed, truncated = context.node._prepare_conversation_history(context.messages) context.trimmed_history = trimmed context.truncated = truncated @then("it should return 2 messages and indicate truncation") def step_assert_history(context): assert len(context.trimmed_history) == 2 assert context.truncated is True assert context.trimmed_history[0]["content"] == "there" assert context.trimmed_history[1]["content"] == "friend" @given("an agent node referencing a missing agent") def step_agent_missing(context): config = NodeConfig(name="agent", type=NodeType.AGENT, agent="missing") context.node = Node(config, agents={}) context.state = _make_state(messages=[{"role": "user", "content": "hi"}]) @when("I execute the agent node") def step_execute_agent(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("it should return a not found assistant message") def step_assert_missing_agent(context): messages = context.result.get("messages", []) assert messages assert messages[0]["content"] == "Agent missing not found" assert messages[0]["agent"] == "missing" @given('a tool agent node with a current_message "{text}" and long history') def step_tool_agent_with_current(context, text): tool_agent = DummyToolAgent("tool", response="ok") config = NodeConfig( name="tool_node", type=NodeType.AGENT, agent="tool", metadata={"max_history_messages": 1}, ) context.node = Node(config, agents={"tool": tool_agent}) long_history = [ {"role": "user", "content": "first"}, {"role": "assistant", "content": "second"}, ] context.state = _make_state( messages=long_history, metadata={"current_message": text} ) @when("I execute the tool agent node") def step_execute_tool_agent(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then('the tool agent should receive input "{text}"') def step_assert_tool_input(context, text): agent = context.node.agents["tool"] assert agent.received_inputs[-1] == text @then("the response metadata should include last_agent_node") def step_assert_last_agent_metadata(context): metadata = context.result.get("metadata", {}) assert metadata.get("last_agent_node") == "tool_node" @given("a function node with an async function") def step_async_function_node(context): async def async_fn(state): return "async-result" config = NodeConfig(name="fn", type=NodeType.FUNCTION, function="async_fn") context.node = Node(config, agents={"async_fn": async_fn}) context.state = _make_state() @given("a function node with a sync function that returns a coroutine") def step_sync_function_coroutine(context): async def inner(): return "sync-inner" def sync_fn(state): return inner() config = NodeConfig(name="fn_sync", type=NodeType.FUNCTION, function="sync_fn") context.node = Node(config, agents={"sync_fn": sync_fn}) context.state = _make_state() @when("I execute the function node") def step_execute_function_node(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then('the function result message should be "{expected}"') def step_assert_function_result(context, expected): messages = context.result.get("messages", []) assert messages assert messages[0]["content"] == expected @given('a conditional node looking for field "{field}" equals "{value}"') def step_conditional_node(context, field, value): config = NodeConfig( name="cond", type=NodeType.CONDITIONAL, condition={"field": field, "equals": value}, ) context.node = Node(config) context.state = _make_state( messages=[{"role": "assistant", field: value, "content": "reply"}] ) @when("I execute the conditional node with matching assistant message") def step_execute_conditional(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then("the condition_result should be true and metadata flagged") def step_assert_conditional(context): assert context.result.get("condition_result") is True metadata = context.result.get("metadata", {}) assert metadata.get("conditional_edges_used") is True @given('a message router node with a rule matching field "{field}" value "{value}"') def step_message_router_node(context, field, value): rules = [{"condition": {"field": field, "value": value}, "target": "urgent_node"}] config = NodeConfig( name="router", type=NodeType.MESSAGE_ROUTER, metadata={"rules": rules} ) context.node = Node(config) context.state = _make_state(metadata={"current_message": {field: value}}) @when('I execute the message router with current_message containing topic "urgent"') def step_execute_message_router(context): loop = asyncio.new_event_loop() try: context.result = loop.run_until_complete(context.node.execute(context.state)) finally: loop.close() @then('it should route to "urgent_node"') def step_assert_router(context): metadata = context.result.get("metadata", {}) assert metadata.get("routed_to") == "urgent_node" @given('an edge with equals condition on field "{field}" value "{value}"') def step_edge_condition(context, field, value): context.edge = Edge( source="a", target="b", condition={"field": field, "equals": value} ) context.node = Node(NodeConfig(name="edge", type=NodeType.END)) @when("I evaluate the edge condition with matching last message") def step_eval_edge(context): state = _make_state(messages=[{"role": "assistant", "content": "done"}]) context.edge_result = context.node.evaluate_edge_condition(context.edge, state) @then("the edge condition should be true") def step_assert_edge_true(context): assert context.edge_result is True @then("evaluating with no messages should also default to true") def step_assert_edge_default_true(context): empty_state = _make_state() assert context.node.evaluate_edge_condition(context.edge, empty_state) is True @given('I evaluate equals condition on message "{text}"') def step_eval_equals(context, text): context.node = Node(NodeConfig(name="eval", type=NodeType.END)) context.eval_equals = context.node._eval_condition(text, {"equals": text}) @then("the equals evaluation should return true") def step_assert_eval_true(context): assert context.eval_equals is True @when('I evaluate field condition on message dict with foo value "bar"') def step_eval_field_true(context): context.eval_field_true = context.node._eval_condition( {"foo": "bar"}, {"field": "foo", "value": "bar"} ) @then("the field evaluation should return true") def step_assert_field_true(context): assert context.eval_field_true is True @when("I evaluate field condition on non-dict message") def step_eval_field_false(context): context.eval_field_false = context.node._eval_condition( "not a dict", {"field": "foo", "value": "bar"} ) @then("it should return false") def step_assert_field_false(context): assert context.eval_field_false is False @given("a node configured for parallel execution") def step_node_parallel(context): config = NodeConfig(name="parallel", type=NodeType.END, parallel=True) context.node = Node(config) @then("can_execute_parallel should return true") def step_assert_parallel(context): assert context.node.can_execute_parallel() is True