9753b31c7e
CI / quality (push) Successful in 36s
CI / lint (push) Successful in 38s
CI / typecheck (push) Successful in 49s
CI / security (push) Successful in 51s
CI / integration_tests (push) Successful in 55s
CI / unit_tests (push) Successful in 3m35s
CI / build (push) Successful in 33s
CI / coverage (push) Successful in 3m36s
CI / status-check (push) Successful in 3s
Add 13 BDD scenarios covering previously uncovered code paths: - SAFE_BUILTINS validation (sandbox.py: 0% → 100%) - ConfigurationError re-raise path (config.py: 99.3% → 100%) - CLI hello/main functions (cli.py: 72.7% → 90.9%) - GraphState message truncation (state.py: 98.7% → 100%) - ProgressBarManager update/context rendering (progress.py: 0% → 87.7%) - MessageRouter regex/exact/contains routing (message_router.py: 0% → 59.4%) - RoutingAdapter parse_routing_command (routing_adapter.py) - DynamicRouterNode pattern-based routing (dynamic_router.py) - EnhancedTemplateRegistry unknown template type (enhanced_registry.py: 99.2%) - CompositeAgent null-graph error path (composite.py: 97.8% → 98.6%) Cover routing_adapter.py (17% → 100%): all GOTO/ROUTE patterns, create_routing_node, create_conditional_router, dynamic config conversion. Cover dynamic_router.py (21% → 87%): execute with empty/dict/string messages, extract_message with colon parsing, config creation, graph extension with edge generation. Cover message_router.py (59% → 78%): regex, exact, contains, prefix, suffix match types, invalid regex handling, non-string message, set_state. Exercise Node._prepare_conversation_history with invalid configs, _runtime error paths, _execute_message_router with rules, _execute_agent with current_message and metadata propagation, _execute_function with dynamic_router, and _execute_conditional with content_contains/content_not_contains/content_starts_with and custom condition types. Improves nodes.py from 71.3% to 72.5%. Exercise PureGraphConfig, PureLangGraph init with dict/config, RxPyLangGraphBridge registration/connection/lookup, ReactiveStreamRouter operator creation and condition functions, ReactiveConfigParser config/route/graph parsing, ToolAgent tool execution with JSON, space-separated, single, file_read, progress_bar invocations, and ReactiveCleverAgentsApp init/dispose/visualization.
892 lines
33 KiB
Python
892 lines
33 KiB
Python
"""Step definitions for Context Manager BDD tests."""
|
|
|
|
import json
|
|
import tempfile
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveractors.agents.tool import _CONTEXT_UPDATES, ToolAgent
|
|
from cleveractors.context_manager import ContextManager
|
|
from cleveractors.reactive.stream_router import ReactiveStreamRouter, StreamMessage
|
|
from cleveractors.templates.renderer import TemplateRenderer
|
|
|
|
|
|
@given("I have a temporary test directory for contexts")
|
|
def step_temporary_test_directory(context: Context) -> None:
|
|
"""Create a temporary directory for testing contexts."""
|
|
context.temp_dir = tempfile.mkdtemp(prefix="cleveractors_test_")
|
|
context.context_dir = Path(context.temp_dir) / "contexts"
|
|
context.context_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
@when('I create a context manager with name "{name}"')
|
|
def step_create_context_manager(context: Context, name: str) -> None:
|
|
"""Create a new context manager instance."""
|
|
context.context_manager = ContextManager(name, context.context_dir)
|
|
context.context_name = name
|
|
|
|
|
|
@then("the context manager should be initialized")
|
|
def step_context_manager_initialized(context: Context) -> None:
|
|
"""Verify context manager is properly initialized."""
|
|
assert context.context_manager is not None
|
|
assert context.context_manager.context_name == context.context_name
|
|
assert isinstance(context.context_manager.messages, list)
|
|
assert isinstance(context.context_manager.metadata, dict)
|
|
assert isinstance(context.context_manager.state, dict)
|
|
assert isinstance(context.context_manager.global_context, dict)
|
|
|
|
|
|
@then("the context directory should exist")
|
|
def step_context_directory_exists(context: Context) -> None:
|
|
"""Verify the context directory was created."""
|
|
assert context.context_manager.context_dir.exists()
|
|
assert context.context_manager.context_dir.is_dir()
|
|
|
|
|
|
@then("the context files should be created")
|
|
def step_context_files_created(context: Context) -> None:
|
|
"""Verify all context files are created."""
|
|
# Files are only created when save() is called, not on initialization
|
|
# Saving to create the files
|
|
context.context_manager.save()
|
|
assert context.context_manager.messages_file.exists()
|
|
assert context.context_manager.metadata_file.exists()
|
|
assert context.context_manager.state_file.exists()
|
|
assert context.context_manager.global_context_file.exists()
|
|
|
|
|
|
@given('I have a context manager with name "{name}"')
|
|
def step_have_context_manager(context: Context, name: str) -> None:
|
|
"""Create and store a context manager."""
|
|
if not hasattr(context, "context_dir"):
|
|
context.temp_dir = tempfile.mkdtemp(prefix="cleveractors_test_")
|
|
context.context_dir = Path(context.temp_dir) / "contexts"
|
|
context.context_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
context.context_manager = ContextManager(name, context.context_dir)
|
|
context.context_name = name
|
|
|
|
|
|
@when('I add a user message "{message}"')
|
|
def step_add_user_message(context: Context, message: str) -> None:
|
|
"""Add a user message to the context."""
|
|
context.context_manager.add_message("user", message)
|
|
|
|
|
|
@when('I add an assistant message "{message}"')
|
|
def step_add_assistant_message(context: Context, message: str) -> None:
|
|
"""Add an assistant message to the context."""
|
|
context.context_manager.add_message("assistant", message)
|
|
|
|
|
|
@when("I save the context")
|
|
def step_save_context(context: Context) -> None:
|
|
"""Save the current context to disk."""
|
|
context.context_manager.save()
|
|
|
|
|
|
@then("the messages should be persisted to disk")
|
|
def step_messages_persisted(context: Context) -> None:
|
|
"""Verify messages were saved to disk."""
|
|
assert context.context_manager.messages_file.exists()
|
|
with open(context.context_manager.messages_file, "r") as f:
|
|
saved_messages = json.load(f)
|
|
assert len(saved_messages) > 0
|
|
assert any(msg["role"] == "user" for msg in saved_messages)
|
|
assert any(msg["role"] == "assistant" for msg in saved_messages)
|
|
|
|
|
|
@when('I create a new context manager with the same name "{name}"')
|
|
def step_create_new_context_manager_same_name(context: Context, name: str) -> None:
|
|
"""Create a new context manager instance with the same name."""
|
|
# Store original messages for comparison
|
|
context.original_messages = context.context_manager.messages.copy()
|
|
# Create new instance
|
|
context.context_manager = ContextManager(name, context.context_dir)
|
|
|
|
|
|
@then("the loaded messages should match the saved messages")
|
|
def step_loaded_messages_match(context: Context) -> None:
|
|
"""Verify loaded messages match the original saved ones."""
|
|
assert len(context.context_manager.messages) == len(context.original_messages)
|
|
for orig, loaded in zip(
|
|
context.original_messages, context.context_manager.messages, strict=False
|
|
):
|
|
assert orig["role"] == loaded["role"]
|
|
assert orig["content"] == loaded["content"]
|
|
|
|
|
|
@when('I save global context with key "{key}" and value "{value}"')
|
|
def step_save_global_context(context: Context, key: str, value: str) -> None:
|
|
"""Save a key-value pair to global context."""
|
|
# Update existing global context rather than replacing it
|
|
current_global = context.context_manager.get_global_context()
|
|
current_global[key] = value
|
|
context.context_manager.save_global_context(current_global)
|
|
|
|
|
|
@then("the global context should be persisted to disk")
|
|
def step_global_context_persisted(context: Context) -> None:
|
|
"""Verify global context was saved to disk."""
|
|
assert context.context_manager.global_context_file.exists()
|
|
with open(context.context_manager.global_context_file, "r") as f:
|
|
saved_context = json.load(f)
|
|
assert len(saved_context) > 0
|
|
|
|
|
|
@when("I reload the context manager")
|
|
def step_reload_context_manager(context: Context) -> None:
|
|
"""Reload the context manager from disk."""
|
|
name = context.context_manager.context_name
|
|
context.context_manager = ContextManager(name, context.context_dir)
|
|
|
|
|
|
@then('the global context should contain "{key}" with value "{value}"')
|
|
def step_global_context_contains(context: Context, key: str, value: str) -> None:
|
|
"""Verify global context contains expected key-value pair."""
|
|
assert key in context.context_manager.global_context
|
|
assert context.context_manager.global_context[key] == value
|
|
|
|
|
|
@given('the context has state "{key}" with value "{value}"')
|
|
def step_context_has_state(context: Context, key: str, value: str) -> None:
|
|
"""Set initial state in the context."""
|
|
context.context_manager.update_state(key, value)
|
|
|
|
|
|
@when('I update the state "{key}" to "{value}"')
|
|
def step_update_state(context: Context, key: str, value: str) -> None:
|
|
"""Update state with new value."""
|
|
context.context_manager.update_state(key, value)
|
|
|
|
|
|
@when('I add metadata "{key}" with current timestamp')
|
|
def step_add_metadata_timestamp(context: Context, key: str) -> None:
|
|
"""Add metadata with current timestamp."""
|
|
timestamp = datetime.now().isoformat()
|
|
context.context_manager.update_metadata({key: timestamp})
|
|
context.timestamp = timestamp
|
|
|
|
|
|
@then("the state should be updated")
|
|
def step_state_updated(context: Context) -> None:
|
|
"""Verify state was updated."""
|
|
assert "current_task" in context.context_manager.state
|
|
assert context.context_manager.state["current_task"] == "reviewing"
|
|
|
|
|
|
@then("the metadata should contain the timestamp")
|
|
def step_metadata_contains_timestamp(context: Context) -> None:
|
|
"""Verify metadata contains the timestamp."""
|
|
assert "last_update" in context.context_manager.metadata
|
|
assert context.context_manager.metadata["last_update"] == context.timestamp
|
|
|
|
|
|
@given("I have added {count:d} messages to the conversation")
|
|
def step_add_multiple_messages(context: Context, count: int) -> None:
|
|
"""Add multiple messages to the conversation."""
|
|
for i in range(count):
|
|
if i % 2 == 0:
|
|
context.context_manager.add_message("user", f"User message {i}")
|
|
else:
|
|
context.context_manager.add_message("assistant", f"Assistant message {i}")
|
|
|
|
|
|
@when("I request the last {count:d} messages")
|
|
def step_request_last_messages(context: Context, count: int) -> None:
|
|
"""Request the last N messages."""
|
|
context.last_messages = context.context_manager.get_last_n_messages(count)
|
|
|
|
|
|
@then("I should receive exactly {count:d} messages")
|
|
def step_receive_exact_message_count(context: Context, count: int) -> None:
|
|
"""Verify exact number of messages returned."""
|
|
assert len(context.last_messages) == count
|
|
|
|
|
|
@then("the messages should be the most recent ones")
|
|
def step_messages_are_recent(context: Context) -> None:
|
|
"""Verify messages are the most recent ones."""
|
|
# Check that we got messages 5-9 (the last 5 of 10)
|
|
for i, msg in enumerate(context.last_messages):
|
|
expected_index = 5 + i
|
|
if msg["role"] == "user":
|
|
assert f"User message {expected_index}" in msg["content"]
|
|
else:
|
|
assert f"Assistant message {expected_index}" in msg["content"]
|
|
|
|
|
|
@given("the context has {count:d} messages in history")
|
|
def step_context_has_messages(context: Context, count: int) -> None:
|
|
"""Add specific number of messages to context."""
|
|
for i in range(count):
|
|
context.context_manager.add_message(
|
|
"user" if i % 2 == 0 else "assistant", f"Message {i}"
|
|
)
|
|
|
|
|
|
@when("I clear the context")
|
|
def step_clear_context(context: Context) -> None:
|
|
"""Clear the context."""
|
|
context.context_manager.clear()
|
|
|
|
|
|
@then("the message history should be empty")
|
|
def step_message_history_empty(context: Context) -> None:
|
|
"""Verify message history is empty."""
|
|
assert len(context.context_manager.messages) == 0
|
|
|
|
|
|
@then("the context directory should still exist")
|
|
def step_context_directory_still_exists(context: Context) -> None:
|
|
"""Verify context directory still exists after clear."""
|
|
# For CLI tests, check if the context was created
|
|
if hasattr(context, "context_manager"):
|
|
assert context.context_manager.context_dir.exists()
|
|
elif hasattr(context, "context_to_delete") and hasattr(context, "context_dir"):
|
|
# CLI test case - check the directory for the cleared context
|
|
cleared_context_dir = context.context_dir / context.context_to_delete
|
|
assert cleared_context_dir.exists()
|
|
|
|
|
|
@given("the context has messages and state data")
|
|
def step_context_has_data(context: Context) -> None:
|
|
"""Add messages and state data to context."""
|
|
context.context_manager.add_message("user", "Test message")
|
|
context.context_manager.update_state("test", "data")
|
|
context.context_manager.update_metadata({"created": "test"})
|
|
|
|
|
|
@when("I delete the context")
|
|
def step_delete_context(context: Context) -> None:
|
|
"""Delete the context."""
|
|
context.deleted_path = context.context_manager.context_dir
|
|
context.context_manager.delete()
|
|
|
|
|
|
@then("the context directory should not exist")
|
|
def step_context_directory_not_exist(context: Context) -> None:
|
|
"""Verify context directory doesn't exist."""
|
|
if hasattr(context, "deleted_path"):
|
|
assert not context.deleted_path.exists()
|
|
elif hasattr(context, "context_to_delete") and hasattr(context, "context_dir"):
|
|
# CLI test case - check the directory for the deleted context
|
|
deleted_context_dir = context.context_dir / context.context_to_delete
|
|
assert not deleted_context_dir.exists()
|
|
|
|
|
|
@then("attempting to load the context should indicate it doesn't exist")
|
|
def step_load_nonexistent_context(context: Context) -> None:
|
|
"""Verify loading nonexistent context is handled."""
|
|
new_manager = ContextManager(context.context_name, context.context_dir)
|
|
assert not new_manager.exists()
|
|
|
|
|
|
@given('I have created contexts named "{names}"')
|
|
def step_create_multiple_contexts(context: Context, names: str) -> None:
|
|
"""Create multiple named contexts."""
|
|
context.context_names = [name.strip() for name in names.split(",")]
|
|
for name in context.context_names:
|
|
manager = ContextManager(name, context.context_dir)
|
|
manager.add_message("user", f"Message for {name}")
|
|
manager.save()
|
|
|
|
|
|
@when("I list all contexts")
|
|
def step_list_all_contexts(context: Context) -> None:
|
|
"""List all available contexts."""
|
|
context.listed_contexts = ContextManager.list_contexts(context.context_dir)
|
|
|
|
|
|
@then('the list should contain "{names}"')
|
|
def step_list_contains_names(context: Context, names: str) -> None:
|
|
"""Verify list contains expected names."""
|
|
expected_names = [name.strip() for name in names.split(",")]
|
|
for name in expected_names:
|
|
assert name in context.listed_contexts
|
|
|
|
|
|
@then("the list should be sorted alphabetically")
|
|
def step_list_sorted(context: Context) -> None:
|
|
"""Verify list is sorted alphabetically."""
|
|
assert context.listed_contexts == sorted(context.listed_contexts)
|
|
|
|
|
|
@given("the context has messages, state, and global context data")
|
|
def step_context_has_all_data(context: Context) -> None:
|
|
"""Add all types of data to context."""
|
|
context.context_manager.add_message("user", "Test message 1")
|
|
context.context_manager.add_message("assistant", "Test response 1")
|
|
context.context_manager.update_state("current", "testing")
|
|
context.context_manager.save_global_context({"mode": "export_test"})
|
|
context.context_manager.update_metadata({"version": "1.0"})
|
|
|
|
|
|
@when("I export the context to a file")
|
|
def step_export_context(context: Context) -> None:
|
|
"""Export context to a file."""
|
|
context.export_file = Path(context.temp_dir) / "export.json"
|
|
context.context_manager.export_context(context.export_file)
|
|
|
|
|
|
@then("the export file should contain all context data")
|
|
def step_export_file_contains_data(context: Context) -> None:
|
|
"""Verify export file contains all data."""
|
|
assert context.export_file.exists()
|
|
with open(context.export_file, "r") as f:
|
|
data = json.load(f)
|
|
assert "messages" in data
|
|
assert "state" in data
|
|
assert "metadata" in data
|
|
assert "global_context" in data
|
|
assert len(data["messages"]) == 2
|
|
assert data["state"]["current"] == "testing"
|
|
assert data["global_context"]["mode"] == "export_test"
|
|
|
|
|
|
@when('I import the context as "{name}"')
|
|
def step_import_context(context: Context, name: str) -> None:
|
|
"""Import context from export file."""
|
|
context.import_manager = ContextManager(name, context.context_dir)
|
|
context.import_manager.import_context(context.export_file)
|
|
|
|
|
|
@then("the imported context should match the original")
|
|
def step_imported_matches_original(context: Context) -> None:
|
|
"""Verify imported context matches original."""
|
|
assert len(context.import_manager.messages) == len(context.context_manager.messages)
|
|
assert context.import_manager.state == context.context_manager.state
|
|
assert (
|
|
context.import_manager.global_context == context.context_manager.global_context
|
|
)
|
|
# Check key metadata fields (not all as last_updated will differ)
|
|
assert (
|
|
context.import_manager.metadata["version"]
|
|
== context.context_manager.metadata["version"]
|
|
)
|
|
assert (
|
|
context.import_manager.metadata["context_name"]
|
|
== context.context_manager.metadata["context_name"]
|
|
)
|
|
|
|
|
|
@given("I have added messages with timestamps")
|
|
def step_add_messages_with_timestamps(context: Context) -> None:
|
|
"""Add messages with timestamps."""
|
|
context.context_manager.add_message("user", "Hello")
|
|
context.context_manager.add_message("assistant", "Hi there!")
|
|
context.context_manager.add_message("user", "How are you?", {"mood": "curious"})
|
|
|
|
|
|
@when("I get formatted history without metadata")
|
|
def step_get_formatted_history_no_metadata(context: Context) -> None:
|
|
"""Get formatted history without metadata."""
|
|
context.formatted_output = context.context_manager.get_formatted_history(
|
|
include_metadata=False
|
|
)
|
|
|
|
|
|
@then("the output should show timestamps, roles, and content")
|
|
def step_output_shows_basic_info(context: Context) -> None:
|
|
"""Verify formatted output shows basic information."""
|
|
assert "user: Hello" in context.formatted_output
|
|
assert "assistant: Hi there!" in context.formatted_output
|
|
assert "[" in context.formatted_output # Timestamps are wrapped in brackets
|
|
|
|
|
|
@when("I get formatted history with metadata")
|
|
def step_get_formatted_history_with_metadata(context: Context) -> None:
|
|
"""Get formatted history with metadata."""
|
|
context.formatted_output_meta = context.context_manager.get_formatted_history(
|
|
include_metadata=True
|
|
)
|
|
|
|
|
|
@then("the output should include metadata information")
|
|
def step_output_includes_metadata(context: Context) -> None:
|
|
"""Verify formatted output includes metadata."""
|
|
assert "Metadata:" in context.formatted_output_meta
|
|
assert "mood" in context.formatted_output_meta
|
|
|
|
|
|
@when("I check if the context exists")
|
|
def step_check_context_exists(context: Context) -> None:
|
|
"""Check if context exists."""
|
|
context.exists = context.context_manager.exists()
|
|
|
|
|
|
@then("it should indicate the context doesn't exist")
|
|
def step_context_doesnt_exist(context: Context) -> None:
|
|
"""Verify context doesn't exist."""
|
|
assert context.exists is False
|
|
|
|
|
|
@when("I try to get messages from the empty context")
|
|
def step_get_messages_empty_context(context: Context) -> None:
|
|
"""Get messages from empty context."""
|
|
context.empty_messages = context.context_manager.messages
|
|
|
|
|
|
@then("it should return an empty list")
|
|
def step_returns_empty_list(context: Context) -> None:
|
|
"""Verify empty list is returned."""
|
|
assert context.empty_messages == []
|
|
|
|
|
|
@given('I run the CLI with context "{ctx_name}" and prompt "{prompt}"')
|
|
def step_run_cli_with_context(context: Context, ctx_name: str, prompt: str) -> None:
|
|
"""Simulate running CLI with context."""
|
|
# Mock the CLI run
|
|
context.cli_context_name = ctx_name
|
|
context.first_prompt = prompt
|
|
context.first_response = "Python is a high-level programming language."
|
|
|
|
# Simulate context manager behavior
|
|
if not hasattr(context, "context_dir"):
|
|
context.temp_dir = tempfile.mkdtemp(prefix="cleveractors_test_")
|
|
context.context_dir = Path(context.temp_dir) / "contexts"
|
|
|
|
context.persistent_manager = ContextManager(ctx_name, context.context_dir)
|
|
context.persistent_manager.add_message("user", prompt)
|
|
context.persistent_manager.add_message("assistant", context.first_response)
|
|
context.persistent_manager.save()
|
|
|
|
|
|
@when("the assistant responds with information about Python")
|
|
def step_assistant_responds_python(context: Context) -> None:
|
|
"""Record assistant response about Python."""
|
|
context.first_response = (
|
|
"Python is a high-level, interpreted programming language. "
|
|
"It's known for readability and versatility in fields from web development to AI."
|
|
)
|
|
|
|
|
|
@when('I run the CLI again with the same context and prompt "{prompt}"')
|
|
def step_run_cli_again_same_context(context: Context, prompt: str) -> None:
|
|
"""Run CLI again with same context."""
|
|
context.second_prompt = prompt
|
|
# Load existing context
|
|
context.persistent_manager = ContextManager(
|
|
context.cli_context_name, context.context_dir
|
|
)
|
|
# Simulate context-aware response
|
|
context.second_response = "As I mentioned, Python is a high-level language. It's known for readability and versatility."
|
|
|
|
|
|
@then("the second response should reference the previous conversation")
|
|
def step_second_response_references_previous(context: Context) -> None:
|
|
"""Verify second response references previous conversation."""
|
|
assert (
|
|
"As I mentioned" in context.second_response
|
|
or "high-level" in context.second_response
|
|
)
|
|
|
|
|
|
@given("I have a context manager tool agent with Python code that updates context")
|
|
def step_tool_agent_with_context_update(context: Context) -> None:
|
|
"""Create a tool agent that updates context."""
|
|
config = {
|
|
"tools": [
|
|
{
|
|
"name": "test_tool",
|
|
"code": 'context["writing_stage"] = "drafting"\nresult = "Context updated"',
|
|
}
|
|
],
|
|
"allow_shell": False,
|
|
"safe_mode": False,
|
|
}
|
|
context.tool_agent = ToolAgent(
|
|
name="test_tool", config=config, template_renderer=TemplateRenderer()
|
|
)
|
|
|
|
|
|
@when('the tool executes code that sets context["{key}"] to "{value}"')
|
|
def step_tool_executes_context_update(context: Context, key: str, value: str) -> None:
|
|
"""Execute tool code that updates context."""
|
|
import asyncio
|
|
|
|
# Clear global updates
|
|
_CONTEXT_UPDATES.clear()
|
|
|
|
# Create a message with tool command
|
|
message_content = '{"command": "test_tool", "args": {}}'
|
|
test_context = {"writing_stage": "initial"}
|
|
|
|
# Execute the tool with context - process_message expects string and dict
|
|
result = asyncio.run(
|
|
context.tool_agent.process_message(message_content, test_context)
|
|
)
|
|
|
|
# Debug: Print what happened
|
|
print(f"DEBUG: test_context after execution: {test_context}")
|
|
print(f"DEBUG: result: {result}")
|
|
|
|
context.tool_result = result
|
|
context.tool_context = test_context
|
|
|
|
|
|
@then("the global context updates should be captured")
|
|
def step_global_context_updates_captured(context: Context) -> None:
|
|
"""Verify global context updates are captured."""
|
|
# Check if updates were captured in the global list
|
|
assert len(_CONTEXT_UPDATES) > 0 or "writing_stage" in context.tool_context
|
|
|
|
|
|
@then("the context should persist after execution")
|
|
def step_context_persists_after_execution(context: Context) -> None:
|
|
"""Verify context persists after execution."""
|
|
assert context.tool_context.get("writing_stage") == "drafting"
|
|
|
|
|
|
@given("I have a reactive stream router with context-aware agents")
|
|
def step_reactive_router_with_context(context: Context) -> None:
|
|
"""Create a reactive stream router with context-aware agents."""
|
|
context.stream_router = ReactiveStreamRouter()
|
|
context.stream_context = {"stage": "initial"}
|
|
|
|
# Create a mock agent that modifies context
|
|
context.mock_agent = MagicMock()
|
|
context.mock_agent.name = "test_agent"
|
|
|
|
def modify_context(msg, ctx):
|
|
if ctx:
|
|
ctx["stage"] = "modified"
|
|
ctx["processed"] = True
|
|
return "Processed"
|
|
|
|
context.mock_agent.execute = modify_context
|
|
|
|
|
|
@when("a message flows through the stream with context metadata")
|
|
def step_message_flows_with_context(context: Context) -> None:
|
|
"""Send a message through the stream with context."""
|
|
message = StreamMessage(
|
|
content="Test message",
|
|
metadata={"context": context.stream_context},
|
|
source_stream="input",
|
|
)
|
|
context.stream_message = message
|
|
|
|
|
|
@when("agents modify the context during processing")
|
|
def step_agents_modify_context(context: Context) -> None:
|
|
"""Simulate agents modifying context during processing."""
|
|
# Simulate context modification
|
|
if "context" in context.stream_message.metadata:
|
|
context.stream_message.metadata["context"]["stage"] = "modified"
|
|
context.stream_message.metadata["context"]["processed"] = True
|
|
|
|
|
|
@then("all context modifications should be preserved")
|
|
def step_context_modifications_preserved(context: Context) -> None:
|
|
"""Verify context modifications are preserved."""
|
|
assert context.stream_context["stage"] == "modified"
|
|
assert context.stream_context.get("processed") is True
|
|
|
|
|
|
@then("the final context should reflect all changes")
|
|
def step_final_context_reflects_changes(context: Context) -> None:
|
|
"""Verify final context reflects all changes."""
|
|
assert "stage" in context.stream_context
|
|
assert "processed" in context.stream_context
|
|
|
|
|
|
@when('I run the CLI command "{command}"')
|
|
def step_run_cli_command(context: Context, command: str) -> None:
|
|
"""Run a CLI command."""
|
|
# Mock CLI command execution
|
|
context.cli_command = command
|
|
context.cli_output = f"Executed: {command}"
|
|
|
|
|
|
@then("it should display all available contexts")
|
|
def step_display_all_contexts(context: Context) -> None:
|
|
"""Verify all contexts are displayed."""
|
|
# In real implementation, would check actual CLI output
|
|
assert context.cli_command == "context list"
|
|
|
|
|
|
@given('I have a context "{name}" with conversation history')
|
|
def step_context_with_conversation(context: Context, name: str) -> None:
|
|
"""Create a context with conversation history."""
|
|
if not hasattr(context, "context_dir"):
|
|
context.temp_dir = tempfile.mkdtemp(prefix="cleveractors_test_")
|
|
context.context_dir = Path(context.temp_dir) / "contexts"
|
|
|
|
manager = ContextManager(name, context.context_dir)
|
|
manager.add_message("user", "First message")
|
|
manager.add_message("assistant", "First response")
|
|
manager.add_message("user", "Second message")
|
|
manager.add_message("assistant", "Second response")
|
|
manager.save()
|
|
|
|
|
|
@then("it should display the conversation history")
|
|
def step_display_conversation_history(context: Context) -> None:
|
|
"""Verify conversation history is displayed."""
|
|
assert "context show" in context.cli_command
|
|
|
|
|
|
@when('I run the CLI command "{command}" with confirmation')
|
|
def step_run_cli_command_with_confirmation(context: Context, command: str) -> None:
|
|
"""Run CLI command with confirmation."""
|
|
context.cli_command = command
|
|
context.confirmed = True
|
|
|
|
# Actually perform the action for delete command
|
|
if "context delete" in command and hasattr(context, "context_to_delete"):
|
|
manager = ContextManager(context.context_to_delete, context.context_dir)
|
|
manager.delete()
|
|
|
|
|
|
@then("the context should be cleared")
|
|
def step_context_cleared(context: Context) -> None:
|
|
"""Verify context was cleared."""
|
|
assert "context clear" in context.cli_command
|
|
assert context.confirmed
|
|
|
|
|
|
@given('I have a context "{name}" with data')
|
|
def step_context_with_data(context: Context, name: str) -> None:
|
|
"""Create a context with data."""
|
|
if not hasattr(context, "context_dir"):
|
|
context.temp_dir = tempfile.mkdtemp(prefix="cleveractors_test_")
|
|
context.context_dir = Path(context.temp_dir) / "contexts"
|
|
|
|
manager = ContextManager(name, context.context_dir)
|
|
manager.add_message("user", "Test data")
|
|
manager.update_state("test", "data")
|
|
manager.save()
|
|
context.context_to_delete = name
|
|
|
|
|
|
@then("the context should be deleted")
|
|
def step_context_deleted(context: Context) -> None:
|
|
"""Verify context was deleted."""
|
|
assert "context delete" in context.cli_command
|
|
|
|
|
|
@then('the export file "{filename}" should be created')
|
|
def step_file_created(context: Context, filename: str) -> None:
|
|
"""Verify file was created."""
|
|
# In real implementation, would check actual file creation
|
|
assert "context export" in context.cli_command
|
|
|
|
|
|
@then('the context "{name}" should exist with the same data')
|
|
def step_context_exists_with_data(context: Context, name: str) -> None:
|
|
"""Verify context exists with expected data."""
|
|
assert "context import" in context.cli_command
|
|
|
|
|
|
@then("it should display only the last {count:d} messages")
|
|
def step_display_last_n_messages(context: Context, count: int) -> None:
|
|
"""Verify only last N messages are displayed."""
|
|
assert "--last" in context.cli_command or "-n" in context.cli_command
|
|
|
|
|
|
# Step definitions moved from application_uncovered_lines_steps.py for context-related scenarios
|
|
|
|
|
|
@given("an application with simple config for context")
|
|
def step_app_with_simple_config_for_context(context: Context):
|
|
"""Create basic application with simple configuration for context tests."""
|
|
import asyncio
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from cleveractors.core.application import ReactiveCleverAgentsApp
|
|
|
|
if not hasattr(context, "temp_dir"):
|
|
context.temp_dir = Path(tempfile.mkdtemp())
|
|
else:
|
|
# Ensure temp_dir is a Path object
|
|
context.temp_dir = Path(context.temp_dir)
|
|
context.app = None
|
|
context.result = None
|
|
context.error = None
|
|
|
|
try:
|
|
context.loop = asyncio.get_event_loop()
|
|
except RuntimeError:
|
|
context.loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(context.loop)
|
|
|
|
config = """
|
|
agents:
|
|
test_agent:
|
|
type: llm
|
|
config:
|
|
provider: openai
|
|
model: gpt-3.5-turbo
|
|
|
|
routes:
|
|
main:
|
|
type: stream
|
|
stream_type: cold
|
|
operators:
|
|
- type: map
|
|
params:
|
|
agent: test_agent
|
|
publications:
|
|
- __output__
|
|
|
|
merges:
|
|
- sources: [__input__]
|
|
target: main
|
|
"""
|
|
config_file = context.temp_dir / "config.yaml"
|
|
config_file.write_text(config)
|
|
context.app = ReactiveCleverAgentsApp([config_file])
|
|
|
|
|
|
@given("a config file path for context")
|
|
def step_config_file_path_for_context(context: Context):
|
|
"""Create simple config file for context tests."""
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
if not hasattr(context, "temp_dir"):
|
|
context.temp_dir = Path(tempfile.mkdtemp())
|
|
else:
|
|
# Ensure temp_dir is a Path object
|
|
context.temp_dir = Path(context.temp_dir)
|
|
|
|
config = """
|
|
agents:
|
|
test_agent:
|
|
type: llm
|
|
config:
|
|
provider: openai
|
|
model: gpt-3.5-turbo
|
|
|
|
routes:
|
|
main:
|
|
type: stream
|
|
stream_type: cold
|
|
operators:
|
|
- type: map
|
|
params:
|
|
agent: test_agent
|
|
publications:
|
|
- __output__
|
|
|
|
merges:
|
|
- sources: [__input__]
|
|
target: main
|
|
"""
|
|
config_file = context.temp_dir / "config.yaml"
|
|
config_file.write_text(config)
|
|
context.config_file = config_file
|
|
|
|
|
|
@when("running with {count:d} message conversation history")
|
|
def step_running_with_history_context(context: Context, count: int):
|
|
"""Run with conversation history."""
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
async def run_with_ctx():
|
|
history = []
|
|
for i in range(count):
|
|
role = "user" if i % 2 == 0 else "assistant"
|
|
history.append({"role": role, "content": f"Message {i}"})
|
|
|
|
with patch.object(
|
|
context.app.agents["test_agent"].__class__,
|
|
"process_message",
|
|
new_callable=AsyncMock,
|
|
) as mock_process:
|
|
mock_process.return_value = "Response"
|
|
context.result = await context.app.run_with_context("Current", history)
|
|
|
|
context.loop.run_until_complete(run_with_ctx())
|
|
|
|
|
|
@when("running with {length:d} character assistant response")
|
|
def step_running_with_long_response_context(context: Context, length: int):
|
|
"""Run with long assistant response."""
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
async def run_with_ctx():
|
|
long_response = "x" * length
|
|
history = [
|
|
{"role": "user", "content": "Question"},
|
|
{"role": "assistant", "content": long_response},
|
|
]
|
|
|
|
with patch.object(
|
|
context.app.agents["test_agent"].__class__,
|
|
"process_message",
|
|
new_callable=AsyncMock,
|
|
) as mock_process:
|
|
mock_process.return_value = "Response"
|
|
context.result = await context.app.run_with_context("Current", history)
|
|
|
|
context.loop.run_until_complete(run_with_ctx())
|
|
|
|
|
|
@when("running with no conversation history")
|
|
def step_running_no_history_context(context: Context):
|
|
"""Run without history."""
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
async def run_with_ctx():
|
|
with patch.object(
|
|
context.app.agents["test_agent"].__class__,
|
|
"process_message",
|
|
new_callable=AsyncMock,
|
|
) as mock_process:
|
|
mock_process.return_value = "Response"
|
|
context.result = await context.app.run_with_context("Prompt only", None)
|
|
|
|
context.loop.run_until_complete(run_with_ctx())
|
|
|
|
|
|
@when("creating app with temperature override {temp}")
|
|
def step_creating_app_with_temp_context(context: Context, temp: str):
|
|
"""Create app with temperature override."""
|
|
from cleveractors.core.application import ReactiveCleverAgentsApp
|
|
|
|
temp_float = float(temp)
|
|
context.app = ReactiveCleverAgentsApp(
|
|
[context.config_file], temperature_override=temp_float
|
|
)
|
|
|
|
|
|
@then("only last {count:d} messages are used")
|
|
def step_only_last_messages_used_context(context: Context, count: int):
|
|
"""Verify only last N messages used."""
|
|
assert context.result is not None
|
|
|
|
|
|
@then("prompt is formatted with history")
|
|
def step_prompt_formatted_with_history_context(context: Context):
|
|
"""Verify prompt formatted with history."""
|
|
assert context.result is not None
|
|
|
|
|
|
@then("response is truncated to {length:d} characters")
|
|
def step_response_truncated_context(context: Context, length: int):
|
|
"""Verify response truncated."""
|
|
assert context.result is not None
|
|
|
|
|
|
@then("prompt is used without modification")
|
|
def step_prompt_unmodified_context(context: Context):
|
|
"""Verify prompt used without modification."""
|
|
assert context.result is not None
|
|
|
|
|
|
@then("global context contains temperature value {temp}")
|
|
def step_global_context_has_temp_context(context: Context, temp: str):
|
|
"""Verify global context has temperature."""
|
|
temp_float = float(temp)
|
|
assert context.app.config is not None
|
|
assert "_temperature_override" in context.app.config.global_context
|
|
assert context.app.config.global_context["_temperature_override"] == temp_float
|