diff --git a/examples/README.md b/examples/README.md index edf1cfa..5b8a327 100644 --- a/examples/README.md +++ b/examples/README.md @@ -59,6 +59,93 @@ agents and tasks on the command line. Run with `just example cli-cluster`. +## Example: Leftovers Chef (using template) + +From the examples above it may have become clear that all of these examples have a recurring structure and mainly come down to configuration. + +In order to make it easier for non-developers or frontend developers to set up new examples with minimal code interaction, you can **use a template structure** to easily define new example instances. + +Let's give an overview of how to use the template to create the Leftovers Chef example. + +### Copy the template files + +To start a new project called "Leftovers Chef", first create new copies of the template files: + +```bash +cp _example_template.py leftovers_chef.py +cp _config_template.py leftovers_chef_config.py +``` + +### Add configuration data + +In the main script `leftovers_chef.py` you only need to update the import on line 15 with the name of the config file. That's all you need to do here, the main updates are isolated to the configuration file. + +In the configuration file `leftovers_chef_config.py` you'll find a template with some dummy info that will help you configure the user input prompts and the cluster attributes they will populate, the initial propmpt for your example, the cluster name and description, the agents and the tasks. This is very similar to what happens in the [trip planner][trip_planner] and [IG post planner][ig_post_planner] examples. + +### Add your example to the setup + +When you've configured your example data, all that is left to do is to make sure you can run your example with the provided setup. + +Add an import to `main.py`: +```python +from leftovers_chef import run_example as run_leftovers_chef_example +``` +and add it to the supported examples map: +```python +EXAMPLES = { + "trip_planner": run_trip_planner_example, + "ig_post_planner": run_ig_post_planner_example, + "cli_cluster": run_cli_cluster_example, + # Add your example + "leftovers_chef": run_leftovers_chef_example, +} +``` + +And lastly, add a `just` recipe to run your example in `example.just`: +```sh +# Runs an example that prompts the user for a description of their meal to prepare +[no-cd] +leftovers-chef: + @__import__('os').system("just containers check") + @__import__('os').system("docker exec -it examples /bin/bash -c \"source .venv/bin/activate && python examples/main.py leftovers_chef\"") +``` + +### Test it out! + +Now test out your cluster by running: +```sh +just example leftovers_chef +``` + +### FAQ + +> What about the requirements to use the template? + +The requirements are the same as mentioned in [Environment Setup](#environment-setup). Nothing additional is required. + +> Why are the prompts in the configuration file regular strings instead of template strings? + +The strings are dynamically formatted into template strings in example script when the cluster attributes can be resolved. + +> What are the pros and cons of using the template? + +You should use the template if you don't want to customise any behaviour. For example if you're not technical, you can still use the template to create agents. In that case it's simply updating configuration. + +A downside is that you don't have the freedom to change anything. For example, if you wanted to add a frontend that delivers user input and displays the results of the cluster execution, you'll need to change the script you're using. You could create a new template for that! + +### What's next? + +To improve upon this example and keep learning, you could build: + +- Add an agent / task that adds a sustainability score to each meal that is suggested. +- Use a tool to: + - Create a generated image of the meal + - Use a web search to find a list of meals + - Find and integrate a 3rd party tool that estimates sustainability impact. +- Further automation to use templates +- Make a template that uses the Python backend to interact with a Javascript frontend +- many more + ## Tools Agents can use tools to enhance their capabilities. Please refer to the [`tools` README][tools_README] @@ -79,3 +166,4 @@ Events allow offchain systems to respond to onchain actions, automating tool exe [trip_planner]: ./trip_planner.py [cli_cluster]: ./cli_cluster.py [design_cluster]: ../onchain/README.md#cluster +[leftovers_chef_config]: ./leftovers_chef_config.py diff --git a/examples/_config_template.py b/examples/_config_template.py new file mode 100644 index 0000000..657cde3 --- /dev/null +++ b/examples/_config_template.py @@ -0,0 +1,42 @@ +from colorama import Fore, Style + +# User input: name of the arguments and the prompt to ask the user for input +# Example input: "name": "What is your name?", +input_config = { + "arg_name": f"{Fore.YELLOW}Put a prompt here for arg_name... {Style.RESET_ALL}", + "another_arg_name": f"{Fore.YELLOW}Put another prompt here for another_arg_name... {Style.RESET_ALL}", +} + +# Initial prompt +# Refer to the the variables you've defined like so, "{variable_name}"". +# String will be formatted in the main program. Leave it as regular string here. +init_prompt = "Put the initial prompt to run the cluster execution here." + +# Setting up the cluster +cluster_config = { + "name": "Put Cluster Name here", + "description": "Put Cluster Description here", +} + +# Agent(s) Configuration +agent_configs = [ + ( + "name", + "role", + "goal", + "backstory", + ), +] + +# Task(s) Configuration +# Refer to the the variables you've defined like so, "{variable_name}"". +# String will be formatted in the main program. Leave it as regular string here. +task_configs = [ + ( + "Put the Task name here", + "Put the Agent name (performing the task) here", + """" + Put the task description here. + """, + ), +] diff --git a/examples/_example_template.py b/examples/_example_template.py new file mode 100644 index 0000000..42df71d --- /dev/null +++ b/examples/_example_template.py @@ -0,0 +1,144 @@ +# Use [run_example] to run the example. +# It's a blocking function that takes a client and package ID as arguments +# and then prompts the user for input. + +import textwrap +from colorama import Fore, Style +from nexus_sdk import ( + create_cluster, + create_agent_for_cluster, + create_task, + execute_cluster, + get_cluster_execution_response, +) + +from _config_template import input_config, agent_configs, task_configs, init_prompt, cluster_config + +class ClusterName: + def __init__( + self, + client, + package_id, + model_id, + model_owner_cap_id, + # Dynamically set attributes based on provided inputs by the user + **kwargs + ): + self.client = client + self.package_id = package_id + self.model_id = model_id + self.model_owner_cap_id = model_owner_cap_id + + # Dynamically set attributes based on provided inputs + for key, value in kwargs.items(): + setattr(self, key, value) + + def setup_cluster(self): + # Create a cluster (equivalent to Crew in CrewAI) + cluster_id, cluster_owner_cap_id = create_cluster( + self.client, + self.package_id, + # Add in the cluster name and description from the config file + cluster_config["name"], + cluster_config["description"], + ) + return cluster_id, cluster_owner_cap_id + + def setup_agents(self, cluster_id, cluster_owner_cap_id): + # Create agents (assuming we have model_ids and model_owner_cap_ids) + # Agents are imported from the agent_configs list + + for agent_name, role, goal in agent_configs: + create_agent_for_cluster( + self.client, + self.package_id, + cluster_id, + cluster_owner_cap_id, + self.model_id, + self.model_owner_cap_id, + agent_name, + role, + goal, + f"An AI agent specialized in {role.lower()}.", + ) + + def setup_tasks(self, cluster_id, cluster_owner_cap_id): + # Create tasks for the agents + # Tasks are imported from the task_configs list + task_ids = [] + for task_name, agent_id, description in task_configs: + task_id = create_task( + self.client, + self.package_id, + cluster_id, + cluster_owner_cap_id, + task_name, + agent_id, + description, + f"Complete {task_name} task", + description, + "", # No specific context provided in this example + ) + task_ids.append(task_id) + + return task_ids + + def run(self): + cluster_id, cluster_owner_cap_id = self.setup_cluster() + self.setup_agents(cluster_id, cluster_owner_cap_id) + self.setup_tasks(cluster_id, cluster_owner_cap_id) + + # Execute the cluster, passing the initial prompt from the config file but formatting the string + execution_id = execute_cluster( + self.client, + self.package_id, + cluster_id, + init_prompt.format(**self.__dict__), + ) + + if execution_id is None: + return "Cluster execution failed" + + print(f"Cluster execution started with ID: {execution_id}") + return get_cluster_execution_response(self.client, execution_id, 600) + + +# Runs the example using the provided Nexus package ID. +def run__example(client, package_id, model_id, mode_owner_cap): + print(f"{Fore.CYAN}## Welcome to Nexus{Style.RESET_ALL}") + print(f"{Fore.YELLOW}-------------------------------{Style.RESET_ALL}") + + # Collect inputs in a dictionary + user_inputs = {key: input(prompt) for key, prompt in input_config.items()} + + cluster = ClusterName( + client, + package_id, + model_id, + mode_owner_cap, + **user_inputs + ) + + print() + result = cluster.run() + + print(f"\n\n{Fore.CYAN}########################{Style.RESET_ALL}") + print(f"{Fore.CYAN}## Here is your Solution{Style.RESET_ALL}") + print(f"{Fore.CYAN}########################\n{Style.RESET_ALL}") + + paginate_output(result) + + +# Helper function to paginate the result output +def paginate_output(text, width=80): + lines = text.split("\n") + + for i, line in enumerate(lines, 1): + wrapped_line = textwrap.fill(line, width) + print(wrapped_line) + + # It's nice when this equals the number of lines in the terminal, using + # default value 32 for now. + pause_every_n_lines = 32 + if i % pause_every_n_lines == 0: + input(f"{Fore.YELLOW}-- Press Enter to continue --{Style.RESET_ALL}") diff --git a/examples/example.just b/examples/example.just index c5d0fe6..9889659 100644 --- a/examples/example.just +++ b/examples/example.just @@ -27,3 +27,9 @@ trip-planner: cli-cluster: @__import__('os').system("just containers check") @__import__('os').system("docker exec -it examples /bin/bash -c \"source .venv/bin/activate && python examples/main.py cli_cluster\"") + +# Runs an example that prompts the user for a description of their meal to prepare +[no-cd] +leftovers-chef: + @__import__('os').system("just containers check") + @__import__('os').system("docker exec -it examples /bin/bash -c \"source .venv/bin/activate && python examples/main.py leftovers_chef\"") \ No newline at end of file diff --git a/examples/leftovers_chef.py b/examples/leftovers_chef.py new file mode 100644 index 0000000..b44e7a1 --- /dev/null +++ b/examples/leftovers_chef.py @@ -0,0 +1,145 @@ +# Use [run_example] to run the example. +# It's a blocking function that takes a client and package ID as arguments +# and then prompts the user for input. + +import textwrap +from colorama import Fore, Style +from nexus_sdk import ( + create_cluster, + create_agent_for_cluster, + create_task, + execute_cluster, + get_cluster_execution_response, +) + +from leftovers_chef_config import input_config, agent_configs, task_configs, init_prompt, cluster_config + +class ClusterName: + def __init__( + self, + client, + package_id, + model_id, + model_owner_cap_id, + # Dynamically set attributes based on provided inputs by the user + **kwargs + ): + self.client = client + self.package_id = package_id + self.model_id = model_id + self.model_owner_cap_id = model_owner_cap_id + + # Dynamically set attributes based on provided inputs + for key, value in kwargs.items(): + setattr(self, key, value) + + def setup_cluster(self): + # Create a cluster (equivalent to Crew in CrewAI) + cluster_id, cluster_owner_cap_id = create_cluster( + self.client, + self.package_id, + # Add in the cluster name and description from the config file + cluster_config["name"], + cluster_config["description"], + ) + return cluster_id, cluster_owner_cap_id + + def setup_agents(self, cluster_id, cluster_owner_cap_id): + # Create agents (assuming we have model_ids and model_owner_cap_ids) + # Agents are imported from the agent_configs list + + for agent_name, role, goal in agent_configs: + create_agent_for_cluster( + self.client, + self.package_id, + cluster_id, + cluster_owner_cap_id, + self.model_id, + self.model_owner_cap_id, + agent_name, + role, + goal, + f"An AI agent specialized in {role.lower()}.", + ) + + def setup_tasks(self, cluster_id, cluster_owner_cap_id): + # Create tasks for the agents + # Tasks are imported from the task_configs list + + task_ids = [] + for task_name, agent_id, description in task_configs: + task_id = create_task( + self.client, + self.package_id, + cluster_id, + cluster_owner_cap_id, + task_name, + agent_id, + description.format(**self.__dict__), + f"Complete {task_name} task", + description.format(**self.__dict__), + "", # No specific context provided in this example + ) + task_ids.append(task_id) + + return task_ids + + def run(self): + cluster_id, cluster_owner_cap_id = self.setup_cluster() + self.setup_agents(cluster_id, cluster_owner_cap_id) + self.setup_tasks(cluster_id, cluster_owner_cap_id) + + # Execute the cluster, passing the initial prompt from the config file but formatting the string + execution_id = execute_cluster( + self.client, + self.package_id, + cluster_id, + init_prompt.format(**self.__dict__), + ) + + if execution_id is None: + return "Cluster execution failed" + + print(f"Cluster execution started with ID: {execution_id}") + return get_cluster_execution_response(self.client, execution_id, 600) + + +# Runs the example using the provided Nexus package ID. +def run_example(client, package_id, model_id, mode_owner_cap): + print(f"{Fore.CYAN}## Welcome to Nexus{Style.RESET_ALL}") + print(f"{Fore.YELLOW}-------------------------------{Style.RESET_ALL}") + + # Collect inputs in a dictionary + user_inputs = {key: input(prompt) for key, prompt in input_config.items()} + + cluster = ClusterName( + client, + package_id, + model_id, + mode_owner_cap, + **user_inputs + ) + + print() + result = cluster.run() + + print(f"\n\n{Fore.CYAN}########################{Style.RESET_ALL}") + print(f"{Fore.CYAN}## Here is your Solution{Style.RESET_ALL}") + print(f"{Fore.CYAN}########################\n{Style.RESET_ALL}") + + paginate_output(result) + + +# Helper function to paginate the result output +def paginate_output(text, width=80): + lines = text.split("\n") + + for i, line in enumerate(lines, 1): + wrapped_line = textwrap.fill(line, width) + print(wrapped_line) + + # It's nice when this equals the number of lines in the terminal, using + # default value 32 for now. + pause_every_n_lines = 32 + if i % pause_every_n_lines == 0: + input(f"{Fore.YELLOW}-- Press Enter to continue --{Style.RESET_ALL}") diff --git a/examples/leftovers_chef_config.py b/examples/leftovers_chef_config.py new file mode 100644 index 0000000..88bdfc4 --- /dev/null +++ b/examples/leftovers_chef_config.py @@ -0,0 +1,66 @@ +from colorama import Fore, Style + +# User input: name of the arguments and the prompt to ask the user +# Example input: "name": "What is your name?", +input_config = { + "leftover_ingredients": f"{Fore.GREEN}What ingredients do you have left over? {Style.RESET_ALL}", + "budget": f"{Fore.YELLOW}What is the budget you want to spend for additional ingredients? {Style.RESET_ALL}", + "effort_level" : f"{Fore.YELLOW}What level of effort do you want to spend perparing the meal? {Style.RESET_ALL}", + "preferences": f"{Fore.RED}Do you have any preferences or dietarty restrictions? {Style.RESET_ALL}", +} + +# Initial prompt +# Refer to the the variables you've defined like so, "{variable_name}"" +init_prompt = """ + Suggest a list of 5 meals based on these leftover ingredients: {leftover_ingredients}. + Consider the following: + - Budget (for purchasing the remaining ingredients, if any): {budget} + - Effort Level: {effort_level} + - Preferences (type of cuisine, dietary restrictions, allergies): {preferences} + Give a sustainability score for each meal and highlight the most sustainable options. +""" + +# Setting up the cluster +cluster_config = { + "name": "Leftovers Chef Cluster", + "description": "A cluster for making a meal out of leftovers", +} + +# Agent(s) Configuration +agent_configs = [ + ( + "meal_designer", + "Meal Design Expert", + "Will design meals based on the leftover ingredients in your fridge", + ), + ( + "chef", + "Chef", + "Provides the full list of ingredients needed and the instructions to prepare the meal", + ), +] + +# Task(s) Configuration +# Refer to the the variables you've defined like so, "{variable_name}"". +# String will be formatted in the main program. Leave it as regular string here. +task_configs = [ + ( + "design_menu", + "meal_designer", + """ + Design a meal that includes, but is not limited to, the leftover ingredients ({leftover_ingredients}) provided as inputs. + Consider the budget, {budget}, effort level, {effort_level}, and preferences, {preferences} of the user. + Suggest in total 5 dishes that are compatible with the inputs. + """, + ), + + ( + "prepare_meal", + "chef", + """ + Provide a detailed recipe for preparing each of the proposed meals provided by the menu designer. + Include a list of all ingredients required, step-by-step instructions, and estimated cooking time. + Specify the cooking equipment needed and the difficulty level ({effort_level}) of the recipe. + """, + ), +] diff --git a/examples/main.py b/examples/main.py index d1a1fd6..1c8fe1f 100644 --- a/examples/main.py +++ b/examples/main.py @@ -8,6 +8,7 @@ from colorama import init as colorama_init from ig_post_planner import run_ig_post_planner_example from trip_planner import run_trip_planner_example +from leftovers_chef import run_example as run_leftovers_chef_example from nexus_sdk import get_sui_client # We know that this script is located in the ./examples directory, so we go @@ -29,6 +30,7 @@ "trip_planner": run_trip_planner_example, "ig_post_planner": run_ig_post_planner_example, "cli_cluster": run_cli_cluster_example, + "leftovers_chef": run_leftovers_chef_example, } @@ -97,7 +99,7 @@ def main(): ) parser.add_argument( "example_name", - help="The name of the example to run. Available examples: trip_planner, ig_post_planner, cli_cluster", + help="The name of the example to run. Available examples: trip_planner, ig_post_planner, cli_cluster, leftovers_chef", ) args = parser.parse_args()