diff --git a/README.md b/README.md index 0b1e34c..1c931e8 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ def step_two(next_output, loop_data): # Run the loop loop_data = start(steps=[step_one, step_two]) +# Pause the loop +pause(loop_data) + +# Unpause the loop +unpause(loop_data) + # Stop the loop stop(loop_data) ``` @@ -102,6 +108,48 @@ None --- +Sure, here are the updated sections for the `pause` and `unpause` functions in your README file. + +--- + +## Function `pause` + +```python +pause(loop_data) +``` + +### Description + +Pauses the loop. When paused, the loop will not execute the next step until it's either stepped using the `step` function or unpaused using the `unpause` function. + +### Parameters + +- `loop_data`: a dictionary containing the `pause_event` which is returned by the `start` function. + +### Returns + +None + +--- + +## Function `unpause` + +```python +unpause(loop_data) +``` + +### Description + +Resumes the loop after it has been paused with the `pause` function. If the loop is not paused, calling this function has no effect. + +### Parameters + +- `loop_data`: a dictionary containing the `pause_event` which is returned by the `start` function. + +### Returns + +None + ## Function `step` ```python diff --git a/agentloop/__init__.py b/agentloop/__init__.py index 307f11e..0f9a324 100644 --- a/agentloop/__init__.py +++ b/agentloop/__init__.py @@ -1,12 +1,12 @@ -from .loop import start, step, stop, loop +from .loop import start, step, stop, loop, pause, unpause from .input import step_with_input_key -from .context import create_context_builders __all__ = [ "start", "stop", "step", + "pause", + "unpause", "loop", "step_with_input_key", - "create_context_builders", ] diff --git a/agentloop/context.py b/agentloop/context.py deleted file mode 100644 index ec3b249..0000000 --- a/agentloop/context.py +++ /dev/null @@ -1,37 +0,0 @@ -from datetime import datetime -import importlib -import os -import sys - - -def create_context_builders(context_dir): - """ - Build a context step function from the context builders in the given directory - - Returns: - context: the context dictionary - """ - context_dir = os.path.abspath(context_dir) - sys.path.insert(0, context_dir) - - context_builders = [] - - for filename in os.listdir(context_dir): - if filename.endswith(".py"): - module = importlib.import_module(f"{filename[:-3]}") - - if hasattr(module, "get_context_builders"): - new_context_builders = module.get_context_builders() - for context_builder in new_context_builders: - context_builders.append(context_builder) - sys.path.remove(context_dir) - - def build_context(context={}): - if context is None: - context = {} - - for context_builder in context_builders: - context = context_builder(context) - return context - - return build_context diff --git a/agentloop/input.py b/agentloop/input.py index 8c27375..ef7dae5 100644 --- a/agentloop/input.py +++ b/agentloop/input.py @@ -1,4 +1,4 @@ -from .loop import stop +from agentloop.loop import stop def step_with_input_key(loop_data): """ diff --git a/agentloop/loop.py b/agentloop/loop.py index 4344024..046ef1c 100644 --- a/agentloop/loop.py +++ b/agentloop/loop.py @@ -1,12 +1,12 @@ import threading -def start(steps, stepped=False): +def start(steps, paused=False): """ Function to start the main loop Args: - stepped: boolean - whether the loop should run one-step-at-a-time. + paused: boolean - whether the loop should run automatically. If paused can be stepped one-step-at-a-time. Defaults to False. Returns: @@ -17,9 +17,13 @@ def start(steps, stepped=False): "stop_event": threading.Event(), "step_event": threading.Event(), "started_event": threading.Event(), + "pause_event": threading.Event(), } - thread = threading.Thread(target=loop, args=(steps, stepped, loop_data)) + if paused: + loop_data["pause_event"].set() + + thread = threading.Thread(target=loop, args=(steps, loop_data)) loop_data["thread"] = thread thread.start() @@ -58,29 +62,45 @@ def step(loop_data): loop_data["step_event"].set() -def loop(steps, stepped=False, loop_data=None): +def pause(loop_data): + """ + Function to pause the loop + + Args: + loop_data: loop data object, created by the start function + + This function does not return any value. + """ + loop_data["pause_event"].set() + + +def unpause(loop_data): + """ + Function to unpause the loop + + Args: + loop_data: loop data object, created by the start function + + This function does not return any value. + """ + loop_data["pause_event"].clear() + + +def loop(steps, loop_data): """ Run the step array in a loop until stopped Args: steps: array of functions to be run in the loop - stepped: boolean - whether the loop should run in stepped mode or not + paused: boolean - whether the loop should run start up paused (can be stepped) or running loop_data: loop data object, created by the start function This function does not return any value. """ - if loop_data is None: - loop_data = { - "stop_event": threading.Event(), - "step_event": threading.Event(), - "started_event": threading.Event(), - } - next_output = None loop_data["started_event"].set() # Indicate that the loop has started while not loop_data["stop_event"].is_set(): for step in steps: - # check how many arguments step takes, 1 or 2? number_of_args = step.__code__.co_argcount if number_of_args == 1: next_output = step(next_output) @@ -88,19 +108,17 @@ def loop(steps, stepped=False, loop_data=None): next_output = step(next_output, loop_data) else: raise ValueError( - "Step function must take 1 or 2 arguments" - "Valid arguments are next_output, loop_data (optional)" + "Step function must take 1 or 2 arguments. " + "Valid arguments are next_output, loop_data (optional). " "Found {} arguments".format(number_of_args) ) - if stepped: - # Wait here until step_event is set - while not loop_data["step_event"].wait(timeout=1): - if loop_data["stop_event"].is_set(): - break - if loop_data["stop_event"].is_set(): - break + + while loop_data["pause_event"].is_set(): + print("Loop paused") + if loop_data["stop_event"].wait(timeout=1): + return + if loop_data["step_event"].wait(timeout=1): loop_data["step_event"].clear() if loop_data["stop_event"].is_set(): break - diff --git a/agentloop/tests/__init__.py b/agentloop/tests/__init__.py new file mode 100644 index 0000000..5630037 --- /dev/null +++ b/agentloop/tests/__init__.py @@ -0,0 +1 @@ +from .loop import * \ No newline at end of file diff --git a/agentloop/tests/loop.py b/agentloop/tests/loop.py new file mode 100644 index 0000000..a3d582c --- /dev/null +++ b/agentloop/tests/loop.py @@ -0,0 +1,109 @@ +import threading +import time + +from agentloop import start, step, stop, pause, unpause + + +def step_one(options_dict, loop_data): + if options_dict is None: + options_dict = {"step_one": 0, "step_two": 0} + # increment step_one + options_dict["step_one"] += 1 + print("step_one: ", options_dict["step_one"]) + return options_dict + + +def step_two(options_dict): + # increment step_two + options_dict["step_two"] += 1 + print("step_two: ", options_dict["step_two"]) + return options_dict + + +def inner_loop(context): + loop_data = start(steps=[step_one, step_two], paused=False) + for _ in range(3): + step(loop_data) + # sleep 1 s + time.sleep(0.1) + stop(loop_data) # Stop the loop + return context + + +def test_inner_loop(): + print("Starting test_start") + loop_data = start(steps=[step_one, inner_loop, step_two], paused=False) + for _ in range(3): + step(loop_data) + # sleep 1 s + time.sleep(0.1) + assert isinstance(loop_data["thread"], threading.Thread) + assert isinstance(loop_data["step_event"], threading.Event) + assert loop_data["thread"].is_alive() is True + stop(loop_data) # Stop the loop + assert loop_data["thread"].is_alive() is False + + +def test_start(): + print("Starting test_start") + loop_data = start(steps=[step_one, step_two], paused=False) + + assert isinstance(loop_data["thread"], threading.Thread) + assert isinstance(loop_data["step_event"], threading.Event) + assert loop_data["thread"].is_alive() is True + stop(loop_data) # Stop the loop + assert loop_data["thread"].is_alive() is False + + +def test_start_paused(): + print("Starting test_start_paused") + loop_data = start(steps=[step_one, step_two], paused=True) + + assert isinstance(loop_data["thread"], threading.Thread) + assert isinstance(loop_data["step_event"], threading.Event) + assert loop_data["thread"].is_alive() is True + for _ in range(5): + step(loop_data) + # sleep 1 s + time.sleep(0.1) + stop(loop_data) # Stop the loop + assert loop_data["thread"].is_alive() is False + + +def test_pause_unpause(): + print("Starting test_pause_unpause") + + shared_dict = {"paused": False} + + def check_pause_status(options_dict, loop_data): + # Modify the shared_dict to reflect pause status + shared_dict["paused"] = loop_data["pause_event"].is_set() + return options_dict + print("loop") + loop_data = start(steps=[step_one, check_pause_status, step_two, check_pause_status], paused=False) + print("loop2") + assert isinstance(loop_data["thread"], threading.Thread) + assert isinstance(loop_data["step_event"], threading.Event) + assert loop_data["thread"].is_alive() is True + print("assert") + # Initially, we're not paused + step(loop_data) + time.sleep(0.1) + assert shared_dict["paused"] == False + + # After pausing, we should be paused + pause(loop_data) + step(loop_data) + shared_dict["paused"] = loop_data["pause_event"].is_set() + time.sleep(1) + assert shared_dict["paused"] == True + + # After unpausing, we should no longer be paused + unpause(loop_data) + step(loop_data) + time.sleep(1) + shared_dict["paused"] = loop_data["pause_event"].is_set() + assert shared_dict["paused"] == False + + stop(loop_data) + assert loop_data["thread"].is_alive() is False diff --git a/publish.sh b/publish.sh deleted file mode 100644 index b258415..0000000 --- a/publish.sh +++ /dev/null @@ -1,44 +0,0 @@ -set -e # if any step fails, kill the entire script and warn the user something went wrong - -# get VERSION from args --version= -VERSION=${1/--version=/} - -export TWINE_USERNAME=${2/--username=/} -export TWINE_PASSWORD=${3/--password=/} - -# in setup.py, replace the line '\tversion=...' with '\tversion={VERSION}' -sed -i.bak "s/version=.*/version='${VERSION}',/" setup.py - -# remove setup.py.bak -rm "setup.py.bak" - -# Read agentloop/__init__.py, find the line that starts with __version__ and replace with '__version__ = "${VERSION}"' -sed -i.bak "s/__version__.*/__version__ = \"${VERSION}\"/" agentloop/__init__.py -rm agentloop/__init__.py.bak - -# Check if these dependencies are installed, and install them if they aren't -pip install twine || echo "Failed to install twine" -pip install wheel || echo "Failed to install wheel" - -# Make sure these work, and stop the script if they error -# Run setup checks and build -python setup.py check || { echo "Setup check failed"; exit 1; } -python setup.py sdist || { echo "Source distribution build failed"; exit 1; } -python setup.py bdist_wheel --universal || { echo "Wheel build failed"; exit 1; } - -# Make sure these work, and stop the script if they error -# Upload to test repo -twine upload dist/agentloop-${VERSION}.tar.gz --repository-url https://test.pypi.org/legacy/ || { echo "Upload to test repo failed"; exit 1; } -pip install --index-url https://test.pypi.org/simple/ agentloop --user || { echo "Installation from test repo failed"; exit 1; } - -# Final upload -twine upload dist/agentloop-${VERSION}.tar.gz || { echo "Final upload failed"; exit 1; } -pip install agentloop --user || { echo "Installation of agentloop failed"; exit 1; } - -git add agentloop/__init__.py -git add setup.py -git commit -m "Updated to ${VERSION} and published" -git push origin main - -# Let the user know that everything completed successfully -echo "Script completed successfully" diff --git a/setup.py b/setup.py index b8e7617..ce41b0a 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name="agentloop", - version="0.1.5", + version="0.2.0", description="A simple, lightweight loop for your agent.", long_description=readme, # added this line long_description_content_type="text/markdown", # and this line diff --git a/test.py b/test.py index d94e063..bacccd2 100644 --- a/test.py +++ b/test.py @@ -1,68 +1 @@ -import threading -import time - -from agentloop import start, step, stop - - -def step_one(options_dict, loop_data): - if options_dict is None: - options_dict = {"step_one": 0, "step_two": 0} - # increment step_one - options_dict["step_one"] += 1 - print("step_one: ", options_dict["step_one"]) - return options_dict - - -def step_two(options_dict, loop_data): - # increment step_two - options_dict["step_two"] += 1 - print("step_two: ", options_dict["step_two"]) - return options_dict - -def inner_loop(context): - loop_data = start(steps=[step_one, step_two], stepped=False) - for _ in range(3): - step(loop_data) - # sleep 1 s - time.sleep(0.1) - stop(loop_data) # Stop the loop - return context - -def test_inner_loop(): - print("Starting test_start") - loop_data = start(steps=[step_one, inner_loop, step_two], stepped=False) - for _ in range(3): - step(loop_data) - # sleep 1 s - time.sleep(0.1) - assert isinstance(loop_data["thread"], threading.Thread) - assert isinstance(loop_data["step_event"], threading.Event) - assert loop_data["thread"].is_alive() is True - stop(loop_data) # Stop the loop - assert loop_data["thread"].is_alive() is False - - -def test_start(): - print("Starting test_start") - loop_data = start(steps=[step_one, step_two], stepped=False) - - assert isinstance(loop_data["thread"], threading.Thread) - assert isinstance(loop_data["step_event"], threading.Event) - assert loop_data["thread"].is_alive() is True - stop(loop_data) # Stop the loop - assert loop_data["thread"].is_alive() is False - - -def test_start_stepped(): - print("Starting test_start_stepped") - loop_data = start(steps=[step_one, step_two], stepped=True) - - assert isinstance(loop_data["thread"], threading.Thread) - assert isinstance(loop_data["step_event"], threading.Event) - assert loop_data["thread"].is_alive() is True - for _ in range(5): - step(loop_data) - # sleep 1 s - time.sleep(0.1) - stop(loop_data) # Stop the loop - assert loop_data["thread"].is_alive() is False +from agentloop.tests import * \ No newline at end of file