From f9e0c26bef837d9fdf22434e0f221d74b70e3b9a Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 31 Mar 2026 22:05:25 -0400 Subject: [PATCH 01/26] clean: use default model --- docs/examples/m_decompose/decompose_using_cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/m_decompose/decompose_using_cli.sh b/docs/examples/m_decompose/decompose_using_cli.sh index dd6215f5a..783a80740 100755 --- a/docs/examples/m_decompose/decompose_using_cli.sh +++ b/docs/examples/m_decompose/decompose_using_cli.sh @@ -1,7 +1,7 @@ #MODEL_ID=granite3.3 #MODEL_ID=granite4 -MODEL_ID=qwen2.5:7b +MODEL_ID=mistral-small3.2 #qwen2.5:7b m decompose run --model-id $MODEL_ID --out-dir ./ --input-file example.txt From b1bc264c3ba580c8440f15ea070cd2e7b63fe7dc Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 31 Mar 2026 22:05:48 -0400 Subject: [PATCH 02/26] add: decomp prompt v3 --- cli/decompose/decompose.py | 6 +- cli/decompose/m_decomp_result_v3.py.jinja2 | 100 +++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 cli/decompose/m_decomp_result_v3.py.jinja2 diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 63b043f61..4b6f80302 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -32,15 +32,17 @@ class DecompVersion(StrEnum): Newer versions must be declared last to ensure ``latest`` always resolves to the most recent template. - Args: + Attributes: latest (str): Sentinel value that resolves to the last declared version. v1 (str): Version 1 of the decomposition pipeline template. + v2 (str): Version 2 of the decomposition pipeline template. + v3 (str): Version 3 of the decomposition pipeline template. """ latest = "latest" v1 = "v1" v2 = "v2" - # v3 = "v3" + v3 = "v3" this_file_dir = Path(__file__).resolve().parent diff --git a/cli/decompose/m_decomp_result_v3.py.jinja2 b/cli/decompose/m_decomp_result_v3.py.jinja2 new file mode 100644 index 000000000..b229806c8 --- /dev/null +++ b/cli/decompose/m_decomp_result_v3.py.jinja2 @@ -0,0 +1,100 @@ +{% if user_inputs -%} +import os +{% endif -%} +import textwrap + +import mellea + +{%- set ns = namespace(need_req=false) -%} +{%- for item in subtasks -%} + {%- for c in item.constraints or [] -%} + {%- if c.val_fn -%} + {%- set ns.need_req = true -%} + {%- endif -%} + {%- endfor -%} +{%- endfor %} + +{%- if ns.need_req %} +from mellea.stdlib.requirements import req +{%- for c in identified_constraints %} +{%- if c.val_fn and c.val_fn_name %} +from validations.{{ c.val_fn_name }} import validate_input as {{ c.val_fn_name }} +{%- endif %} +{%- endfor %} +{%- endif %} + +m = mellea.start_session() +{%- if user_inputs %} + + +# User Input Variables +try: + {%- for var in user_inputs %} + {{ var | lower }} = os.environ["{{ var | upper }}"] + {%- endfor %} +except KeyError as e: + raise SystemExit(f"ERROR: One or more required environment variables are not set: {e}") +{%- endif %} +{%- for item in subtasks %} + + +{{ item.tag | lower }}_gnrl = textwrap.dedent( + R""" + {{ item.general_instructions | trim | indent(width=4, first=False) }} + """.strip() +) +{{ item.tag | lower }} = m.instruct( + {%- if not (item.input_vars_required or []) %} + {{ item.subtask[3:] | trim | tojson }}, + {%- else %} + textwrap.dedent( + R""" + {{ item.subtask[3:] | trim }} + + Here are the input variables and their content: + {%- for var in item.input_vars_required or [] %} + + - {{ var | upper }} = {{ "{{" }}{{ var | upper }}{{ "}}" }} + {%- endfor %} + """.strip() + ), + {%- endif %} + {%- if item.constraints %} + requirements=[ + {%- for c in item.constraints %} + {%- if c.val_fn and c.val_fn_name %} + req( + {{ c.constraint | tojson}}, + validation_fn={{ c.val_fn_name }}, + ), + {%- else %} + {{ c.constraint | tojson}}, + {%- endif %} + {%- endfor %} + ], + {%- else %} + requirements=None, + {%- endif %} + {%- if item.input_vars_required %} + user_variables={ + {%- for var in item.input_vars_required or [] %} + {{ var | upper | tojson }}: {{ var | lower }}, + {%- endfor %} + }, + {%- endif %} + grounding_context={ + "GENERAL_INSTRUCTIONS": {{ item.tag | lower }}_gnrl, + {%- for var in item.depends_on or [] %} + {{ var | upper | tojson }}: {{ var | lower }}.value, + {%- endfor %} + }, +) +assert {{ item.tag | lower }}.value is not None, 'ERROR: task "{{ item.tag | lower }}" execution failed' +{%- if loop.last %} + + +final_answer = {{ item.tag | lower }}.value + +print(final_answer) +{%- endif -%} +{%- endfor -%} From c2818910df64ae96009f2e96089298fef52cc1d1 Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 31 Mar 2026 22:06:14 -0400 Subject: [PATCH 03/26] upd: enhance general instruction module prompt --- .../_general_instructions.py | 60 ++++++++++++------- .../_prompt/system_template.jinja2 | 4 +- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/cli/decompose/prompt_modules/general_instructions/_general_instructions.py b/cli/decompose/prompt_modules/general_instructions/_general_instructions.py index 26b51c43a..568734ff2 100644 --- a/cli/decompose/prompt_modules/general_instructions/_general_instructions.py +++ b/cli/decompose/prompt_modules/general_instructions/_general_instructions.py @@ -13,10 +13,20 @@ T = TypeVar("T") RE_GENERAL_INSTRUCTIONS = re.compile( - r"(.+?)", + r"(.*?)", flags=re.IGNORECASE | re.DOTALL, ) +RE_GENERAL_INSTRUCTIONS_OPEN = re.compile( + r"(.*)", + flags=re.IGNORECASE | re.DOTALL, +) + +RE_FINAL_SENTENCE = re.compile( + r"\n*All tags are closed and my assignment is finished\.\s*$", + flags=re.IGNORECASE, +) + @final class _GeneralInstructions(PromptModule): @@ -24,16 +34,24 @@ class _GeneralInstructions(PromptModule): def _default_parser(generated_str: str) -> str: general_instructions_match = re.search(RE_GENERAL_INSTRUCTIONS, generated_str) - general_instructions_str: str | None = ( - general_instructions_match.group(1).strip() - if general_instructions_match - else None - ) - - if general_instructions_str is None: - raise TagExtractionError( - 'LLM failed to generate correct tags for extraction: ""' + if general_instructions_match: + general_instructions_str = general_instructions_match.group(1).strip() + else: + # fallback: opening tag only (in case the closing tag is missing) + general_instructions_match = re.search( + RE_GENERAL_INSTRUCTIONS_OPEN, generated_str ) + if not general_instructions_match: + raise TagExtractionError( + 'LLM failed to generate correct tags for extraction: ""' + ) + general_instructions_str = general_instructions_match.group(1).strip() + + general_instructions_str = re.sub( + RE_FINAL_SENTENCE, + "", + general_instructions_str, + ).strip() return general_instructions_str @@ -50,20 +68,22 @@ def generate( system_prompt = get_system_prompt() user_prompt = get_user_prompt(task_prompt=input_str) - action = Message("user", user_prompt) + model_options = { + ModelOption.SYSTEM_PROMPT: system_prompt, + ModelOption.TEMPERATURE: 0, + ModelOption.MAX_NEW_TOKENS: max_new_tokens, + } + try: - gen_result = mellea_session.act( + response = mellea_session.act( action=action, - model_options={ - ModelOption.SYSTEM_PROMPT: system_prompt, - ModelOption.TEMPERATURE: 0, - ModelOption.MAX_NEW_TOKENS: max_new_tokens, - }, - ).value + model_options=model_options, + ) + gen_result = response.value except Exception as e: - raise BackendGenerationError(f"LLM generation failed: {e}") + raise BackendGenerationError(f"LLM generation failed: {e}") from e if gen_result is None: raise BackendGenerationError( @@ -73,4 +93,4 @@ def generate( return PromptModuleString(gen_result, parser) -general_instructions = _GeneralInstructions() +general_instructions = _GeneralInstructions() \ No newline at end of file diff --git a/cli/decompose/prompt_modules/general_instructions/_prompt/system_template.jinja2 b/cli/decompose/prompt_modules/general_instructions/_prompt/system_template.jinja2 index 58047c155..d806cf6bc 100644 --- a/cli/decompose/prompt_modules/general_instructions/_prompt/system_template.jinja2 +++ b/cli/decompose/prompt_modules/general_instructions/_prompt/system_template.jinja2 @@ -13,7 +13,7 @@ Do not write anything between and the final sentence exc Here are some complete examples to guide you on how to complete your assignment: {% for item in icl_examples -%} - + {{ item["task_prompt"] }} @@ -22,7 +22,7 @@ Here are some complete examples to guide you on how to complete your assignment: All tags are closed and my assignment is finished. - + {% endfor -%} That concludes the complete examples of your assignment. From 445ce455fd1d17cbf87becc74624a003aa66ce38 Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 31 Mar 2026 22:06:37 -0400 Subject: [PATCH 04/26] upd: enhance subtask constraint assign module prompt --- .../_subtask_constraint_assign.py | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py index 45f56df0a..b5fb1ec0b 100644 --- a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py +++ b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py @@ -19,7 +19,7 @@ ) RE_ASSIGNED_CONS = re.compile( - r"(.+?)", + r"(.*?)", flags=re.IGNORECASE | re.DOTALL, ) @@ -92,18 +92,22 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: subtask_constraint_assign_match = re.search(RE_ASSIGNED_CONS, data[3]) - subtask_constraint_assign_str: str | None = ( + # ===== fallback: use raw text when there is no tag ===== + subtask_constraint_assign_str: str = ( subtask_constraint_assign_match.group(1).strip() if subtask_constraint_assign_match - else None + else data[3].strip() ) - if subtask_constraint_assign_str is None: - raise TagExtractionError( - 'LLM failed to generate correct tags for extraction: ""' - ) + subtask_constraint_assign_str = re.sub( + r"\n*All tags are closed and my assignment is finished\.\s*$", + "", + subtask_constraint_assign_str, + flags=re.IGNORECASE, + ).strip() subtask_constraint_assign_str_upper = subtask_constraint_assign_str.upper() + if ( "N/A" in subtask_constraint_assign_str_upper or "N / A" in subtask_constraint_assign_str_upper @@ -112,10 +116,22 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: ): subtask_constraint_assign = [] else: - subtask_constraint_assign = [ - line.strip()[2:] if line.strip()[:2] == "- " else line.strip() - for line in subtask_constraint_assign_str.splitlines() - ] + subtask_constraint_assign = [] + for line in subtask_constraint_assign_str.splitlines(): + stripped = line.strip() + + if not stripped: + continue + + # Only keep lines starting with "- " + if stripped.startswith("- "): + value = stripped[2:].strip() + if value: + subtask_constraint_assign.append(value) + continue + + # Remove duplicates while preserving order + subtask_constraint_assign = list(dict.fromkeys(subtask_constraint_assign)) result.append( SubtaskPromptConstraintsItem( From 1e39de15f2d8cdf0cbc8d705f22cd9b77d5edf6c Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 31 Mar 2026 23:12:31 -0400 Subject: [PATCH 05/26] upd: setup the default model --- cli/decompose/m_decomp_result_v3.py.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/decompose/m_decomp_result_v3.py.jinja2 b/cli/decompose/m_decomp_result_v3.py.jinja2 index b229806c8..5e791c29d 100644 --- a/cli/decompose/m_decomp_result_v3.py.jinja2 +++ b/cli/decompose/m_decomp_result_v3.py.jinja2 @@ -23,7 +23,7 @@ from validations.{{ c.val_fn_name }} import validate_input as {{ c.val_fn_name } {%- endfor %} {%- endif %} -m = mellea.start_session() +m = mellea.start_session(model_id="mistral-small3.2:latest") {%- if user_inputs %} From 84027cc1e221cf771e842e9687324616cc5fe86c Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 31 Mar 2026 23:15:05 -0400 Subject: [PATCH 06/26] clean: deleted dump results --- .../python/python_decompose_example.py | 228 ------------------ .../python/python_decompose_final_output.txt | 45 ---- .../python/python_decompose_result.json | 121 ---------- .../python/python_decompose_result.py | 222 ----------------- 4 files changed, 616 deletions(-) delete mode 100644 docs/examples/m_decompose/python/python_decompose_example.py delete mode 100644 docs/examples/m_decompose/python/python_decompose_final_output.txt delete mode 100644 docs/examples/m_decompose/python/python_decompose_result.json delete mode 100644 docs/examples/m_decompose/python/python_decompose_result.py diff --git a/docs/examples/m_decompose/python/python_decompose_example.py b/docs/examples/m_decompose/python/python_decompose_example.py deleted file mode 100644 index d8d31ad23..000000000 --- a/docs/examples/m_decompose/python/python_decompose_example.py +++ /dev/null @@ -1,228 +0,0 @@ -# pytest: ollama, llm, slow, qualitative -#!/usr/bin/env python3 -""" -Example: Using Mellea's decompose functionality programmatically - -This script demonstrates how to use the decompose pipeline from Python code -to break down a complex task into subtasks with generated prompts. -""" - -import json -import subprocess -import textwrap -from pathlib import Path - -from jinja2 import Environment, FileSystemLoader - -# Import the decompose pipeline from the CLI module -from cli.decompose.pipeline import DecompBackend, DecompPipelineResult, decompose - - -def run_decompose(task_prompt: str) -> DecompPipelineResult: - """ - Run the decompose pipeline on a task prompt. - - Args: - task_prompt: The task description to decompose - - Returns: - Dictionary containing decomposition results - """ - print("Running decomposition pipeline...\n") - - result = decompose( - task_prompt=task_prompt, - model_id="granite3.3:8b", # Note micro will not properly create tags, need 8b - backend=DecompBackend.ollama, # Use Ollama backend - backend_req_timeout=300, # 5 minute timeout - ) - - return result - - -def save_decompose_json( - result: DecompPipelineResult, - output_dir: Path, - filename: str = "python_decompose_result.json", -) -> Path: - """ - Save decomposition results to a JSON file. - - Args: - result: Decomposition results dictionary - output_dir: Directory to save the file - filename: Name of the output file - - Returns: - Path to the saved JSON file - """ - json_output_file = output_dir / filename - - with open(json_output_file, "w") as f: - json.dump(result, f, indent=2) - - print(f"💾 JSON results saved to: {json_output_file}") - return json_output_file - - -def generate_python_script( - result: DecompPipelineResult, - output_dir: Path, - filename: str = "python_decompose_result.py", -) -> Path: - """ - Generate an executable Python script from decomposition results. - - Args: - result: Decomposition results dictionary - output_dir: Directory to save the file - filename: Name of the output Python file - - Returns: - Path to the generated Python script - """ - print("\n📝 Generating executable Python script...") - - # Load the template from the CLI decompose directory - cli_decompose_dir = ( - Path(__file__).parent.parent.parent.parent.parent / "cli" / "decompose" - ) - environment = Environment( - loader=FileSystemLoader(cli_decompose_dir), autoescape=False - ) - m_template = environment.get_template("m_decomp_result_v1.py.jinja2") - - # Render the template with the decomposition results - python_script_content = m_template.render( - subtasks=result["subtasks"], - user_inputs=[], # No user inputs for this simple example - ) - - # Save the generated Python script - py_output_file = output_dir / filename - with open(py_output_file, "w") as f: - f.write(python_script_content + "\n") - - print(f"💾 Generated Python script saved to: {py_output_file}") - return py_output_file - - -def run_generated_script( - script_path: Path, output_dir: Path, timeout: int = 600 -) -> Path | None: - """ - Execute the generated Python script to produce final output. - - Args: - script_path: Path to the Python script to execute - output_dir: Directory to save the final output - timeout: Maximum execution time in seconds - - Returns: - Path to the final output file if successful, None otherwise - """ - print("\n🚀 Running the generated script to produce final output...") - print(" (This may take a few minutes as it calls the LLM for each subtask)") - - try: - result_output = subprocess.run( - ["python3", str(script_path)], - capture_output=True, - text=True, - timeout=timeout, - cwd=output_dir, - ) - - if result_output.returncode == 0: - # Save the final output - final_output_file = output_dir / "python_decompose_final_output.txt" - with open(final_output_file, "w") as f: - f.write(result_output.stdout) - - print(f"✅ Final output saved to: {final_output_file}") - print("\n" + "=" * 70) - print("Final Output:") - print("=" * 70) - preview = result_output.stdout - print(preview) - return final_output_file - else: - print( - f"❌ Script execution failed with return code {result_output.returncode}" - ) - print(f"Error: {result_output.stderr}") - return None - except subprocess.TimeoutExpired: - print(f"⏱️ Script execution timed out after {timeout} seconds") - return None - except Exception as e: - print(f"❌ Error running script: {e}") - return None - - -def display_results(result: DecompPipelineResult): - """ - Display decomposition results in a formatted way. - - Args: - result: Decomposition results dictionary - """ - print("=" * 70) - print("Decomposition Results") - print("=" * 70) - - print(f"\n📋 Subtasks Identified ({len(result['subtask_list'])}):") - for i, subtask in enumerate(result["subtask_list"], 1): - print(f" {subtask}") - - print(f"\n🔍 Constraints Identified ({len(result['identified_constraints'])}):") - for i, constraint in enumerate(result["identified_constraints"], 1): - print(f" {i}. {constraint['constraint']}") - print(f" Validation: {constraint['val_strategy']}") - - print(f"\n🎯 Detailed Subtasks ({len(result['subtasks'])}):") - for i, subtask_detail in enumerate(result["subtasks"], 1): - print(f"\n Subtask {subtask_detail['subtask']}") - print(f" Tag: {subtask_detail['tag']}") - print(f" Dependencies: {subtask_detail['depends_on'] or 'None'}") - print(f" Input Variables: {subtask_detail['input_vars_required'] or 'None'}") - print(f" Constraints: {len(subtask_detail['constraints'])}") - - -def main(): - # Define a simple task prompt to decompose - task_prompt = textwrap.dedent(""" - Write a short blog post about the benefits of morning exercise. - Include a catchy title, an introduction paragraph, three main benefits - with explanations, and a conclusion that encourages readers to start - their morning exercise routine. - """).strip() - - print("=" * 70) - print("Mellea Decompose Example") - print("=" * 70) - print(f"\nOriginal Task:\n\n{task_prompt.strip()}\n") - - # Step 1: Run decomposition - result = run_decompose(task_prompt) - - # Step 2: Display results - display_results(result) - - # Step 3: Save JSON results - output_dir = Path(__file__).parent - save_decompose_json(result, output_dir) - - # Step 4: Generate Python script - script_path = generate_python_script(result, output_dir) - - # Step 5: Run the generated script (optional) - run_generated_script(script_path, output_dir) - - print("\n" + "=" * 70) - print("✅ Decomposition complete!") - print("=" * 70) - - -if __name__ == "__main__": - main() diff --git a/docs/examples/m_decompose/python/python_decompose_final_output.txt b/docs/examples/m_decompose/python/python_decompose_final_output.txt deleted file mode 100644 index 76ac65b58..000000000 --- a/docs/examples/m_decompose/python/python_decompose_final_output.txt +++ /dev/null @@ -1,45 +0,0 @@ -=== 12:28:43-INFO ====== -Starting Mellea session: backend=ollama, model=granite4:micro, context=SimpleContext -=== 12:28:45-INFO ====== -SUCCESS -=== 12:28:47-INFO ====== -FAILED. Valid: 0/1 -=== 12:28:48-INFO ====== -FAILED. Valid: 0/1 -=== 12:28:48-INFO ====== -Invoking select_from_failure after 2 failed attempts. -=== 12:28:55-INFO ====== -SUCCESS -=== 12:28:59-INFO ====== -SUCCESS -=== 12:29:10-INFO ====== -FAILED. Valid: 3/4 -=== 12:29:19-INFO ====== -FAILED. Valid: 3/4 -=== 12:29:19-INFO ====== -Invoking select_from_failure after 2 failed attempts. -**Title:** Kickstart Your Day: Unleash Morning Exercise's Power! - -**Introduction:** -Are you tired of feeling sluggish first thing in the morning? Imagine starting your day with a burst of energy and positivity, setting the tone for everything else to come. Morning exercise isn't just about getting fit; it's a powerful way to enhance mental clarity, boost mood, and set a productive tone for the entire day. This blog post, "Kickstart Your Day: Unleash Morning Exercise's Power!" will explore how incorporating morning workouts into your routine can transform not only your physical health but also significantly impact your overall well-being and productivity. - -**Benefits:** - -#### 1. **Enhanced Mental Clarity and Cognitive Function** -Engaging in morning exercise significantly boosts mental clarity, allowing individuals to approach their day with a focused mind. This heightened sense of awareness can lead to improved decision-making skills throughout the day, enhancing productivity and efficiency at work or school. Research from the *International Journal of Behavioral Nutrition and Physical Activity* indicates that regular morning exercise correlates with lower levels of cortisol, the stress hormone, leading to reduced anxiety and depression symptoms. - -#### 2. **Improved Mood and Reduced Stress Levels** -Morning exercise is known for its ability to elevate mood significantly, reducing symptoms of anxiety and depression. By boosting serotonin levels—often referred to as the "feel-good" hormone—morning workouts can set a positive tone for the entire day, reducing stress and enhancing overall emotional well-being. - -#### 3. **Boosted Energy Levels Throughout the Day** -Morning exercise is a powerful way to increase energy levels for several hours post-workout, allowing for sustained productivity throughout the day. This surge in energy can lead to improved performance in daily activities, whether it's meeting deadlines at work or spending quality time with family and friends. - -**Conclusion:** - -Incorporating morning exercise into your daily routine can significantly enhance various aspects of health and well-being, from mental clarity to improved mood and sustained energy levels. As highlighted in the blog post "Kickstart Your Day: Unleash Morning Exercise's Power!", these benefits not only transform physical fitness but also have a profound impact on overall productivity and happiness. - -The key themes we've explored—enhanced cognitive function, improved emotional health, and boosted vitality—underscore why morning exercise is not merely an activity but a transformative habit. By aligning your physical routine with your body's natural rhythms in the morning, you're essentially equipping yourself to tackle the day head-on, more focused, motivated, and ready to embrace whatever challenges come your way. - -Now, imagine waking up each day feeling energized, clear-headed, and eager to make a difference. This is not just wishful thinking—it's achievable with consistent morning exercise. So why wait? Let this be the start of your journey towards a more vibrant, productive, and fulfilling life. Embrace the power of mornings, embrace the power within you, and watch as every day transforms into an opportunity to excel. - -Take that first step today. Whether it's a brisk walk, a yoga session, or a high-intensity workout, the key is starting and making it a part of your daily routine. Your future self will thank you for choosing to wake up not just earlier but also more energized and ready to seize the day. Let morning exercise be the catalyst that propels you towards a healthier, happier, and more productive lifestyle. diff --git a/docs/examples/m_decompose/python/python_decompose_result.json b/docs/examples/m_decompose/python/python_decompose_result.json deleted file mode 100644 index 8a2903782..000000000 --- a/docs/examples/m_decompose/python/python_decompose_result.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "original_task_prompt": "Write a short blog post about the benefits of morning exercise.\nInclude a catchy title, an introduction paragraph, three main benefits\nwith explanations, and a conclusion that encourages readers to start\ntheir morning exercise routine.", - "subtask_list": [ - "1. Create a catchy title for the blog post about the benefits of morning exercise. -", - "2. Write an introduction paragraph that sets the stage for the blog post. -", - "3. Identify and explain three main benefits of morning exercise with detailed explanations. -", - "4. Write a conclusion that encourages readers to start their morning exercise routine. -", - "5. Compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post. -" - ], - "identified_constraints": [ - { - "constraint": "Include a catchy title", - "validation_strategy": "llm" - }, - { - "constraint": "Include an introduction paragraph", - "validation_strategy": "llm" - }, - { - "constraint": "Include three main benefits with explanations", - "validation_strategy": "llm" - }, - { - "constraint": "Include a conclusion that encourages readers to start their morning exercise routine", - "validation_strategy": "llm" - } - ], - "subtasks": [ - { - "subtask": "1. Create a catchy title for the blog post about the benefits of morning exercise. -", - "tag": "BLOG_TITLE", - "constraints": [ - { - "constraint": "Include a catchy title", - "validation_strategy": "llm" - } - ], - "prompt_template": "Your task is to create a catchy title for a blog post about the benefits of morning exercise. Follow these steps to accomplish your task:\n\n1. **Understand the Topic**: The blog post will focus on the benefits of morning exercise. The title should be engaging and clearly convey the main topic of the post.\n\n2. **Identify Key Elements**: Consider the key elements that make morning exercise beneficial. These could include improved mood, increased energy, better focus, and enhanced metabolism.\n\n3. **Use Power Words**: Incorporate power words that evoke curiosity, excitement, or a sense of urgency. Examples include \"Boost,\" \"Transform,\" \"Unlock,\" \"Energize,\" and \"Revitalize.\"\n\n4. **Keep It Concise**: The title should be short and to the point, ideally between 5 to 10 words. It should be easy to read and remember.\n\n5. **Make It Action-Oriented**: Use verbs that encourage action, such as \"Start,\" \"Jumpstart,\" \"Kickstart,\" or \"Ignite.\"\n\n6. **Consider SEO**: Think about common search terms related to morning exercise. Including relevant keywords can help improve the post's visibility.\n\n7. **Examples for Inspiration**:\n - \"Jumpstart Your Day: The Power of Morning Exercise\"\n - \"Energize Your Mornings: Unlock the Benefits of Morning Exercise\"\n - \"Transform Your Day with Morning Exercise\"\n - \"Boost Your Energy: The Magic of Morning Workouts\"\n - \"Revitalize Your Mornings: The Benefits of Morning Exercise\"\n\n8. **Create the Title**: Based on the above guidelines, create a catchy and engaging title for the blog post. Ensure it captures the essence of the topic and entices readers to click and read more.\n\nYour final answer should be only the title text.", - "input_vars_required": [], - "depends_on": [] - }, - { - "subtask": "2. Write an introduction paragraph that sets the stage for the blog post. -", - "tag": "INTRODUCTION", - "constraints": [ - { - "constraint": "Include an introduction paragraph", - "validation_strategy": "llm" - } - ], - "prompt_template": "Your task is to write an engaging introduction paragraph for a blog post about the benefits of morning exercise. The introduction should set the stage for the blog post, capturing the reader's attention and providing a brief overview of what will be discussed.\n\nTo accomplish this, follow these steps:\n\n1. **Understand the Context**:\n - The blog post is about the benefits of morning exercise.\n - The title of the blog post is: {{BLOG_TITLE}}\n\n2. **Craft the Introduction**:\n - Start with a hook that grabs the reader's attention. This could be a question, a surprising fact, or a relatable scenario.\n - Briefly introduce the topic of morning exercise and why it is important.\n - Provide a smooth transition to the main benefits that will be discussed in the blog post.\n\n3. **Ensure Engagement**:\n - Use a conversational and engaging tone to connect with the readers.\n - Keep the introduction concise and to the point, ideally between 3 to 5 sentences.\n\nHere is an example structure to guide your writing:\n- **Sentence 1**: Hook to grab the reader's attention.\n- **Sentence 2**: Introduce the topic of morning exercise.\n- **Sentence 3**: Briefly mention the benefits that will be discussed.\n- **Sentence 4**: Transition to the main content of the blog post.\n\nEnsure that the introduction flows naturally and sets the stage for the rest of the blog post. You should write only the introduction paragraph, do not include the guidance structure.", - "input_vars_required": [], - "depends_on": [ - "BLOG_TITLE" - ] - }, - { - "subtask": "3. Identify and explain three main benefits of morning exercise with detailed explanations. -", - "tag": "BENEFITS", - "constraints": [ - { - "constraint": "Include three main benefits with explanations", - "validation_strategy": "llm" - } - ], - "prompt_template": "Your task is to identify and explain three main benefits of morning exercise with detailed explanations. Follow these steps to accomplish your task:\n\nFirst, review the title and introduction created in the previous steps to understand the context and tone of the blog post:\n\n{{BLOG_TITLE}}\n\n\n{{INTRODUCTION}}\n\n\nNext, research and identify three main benefits of morning exercise. These benefits should be supported by evidence or expert opinions to ensure credibility.\n\nFor each benefit, provide a detailed explanation that includes:\n- The specific benefit of morning exercise\n- How this benefit positively impacts health, well-being, or daily life\n- Any relevant studies, expert opinions, or personal anecdotes that support the benefit\n\nEnsure that the explanations are clear, concise, and engaging to keep the reader interested.\n\nFinally, present the three main benefits with their detailed explanations in a structured format that can be easily integrated into the blog post.", - "input_vars_required": [], - "depends_on": [ - "BLOG_TITLE", - "INTRODUCTION" - ] - }, - { - "subtask": "4. Write a conclusion that encourages readers to start their morning exercise routine. -", - "tag": "CONCLUSION", - "constraints": [ - { - "constraint": "Include a conclusion that encourages readers to start their morning exercise routine", - "validation_strategy": "llm" - } - ], - "prompt_template": "Your task is to write a compelling conclusion for a blog post about the benefits of morning exercise. The conclusion should encourage readers to start their morning exercise routine. Follow these steps to accomplish your task:\n\nFirst, review the title and introduction of the blog post to understand the context and tone:\n\n{{BLOG_TITLE}}\n\n\n{{INTRODUCTION}}\n\n\nNext, consider the three main benefits of morning exercise that have been previously identified and explained:\n\n{{BENEFITS}}\n\n\nUse the information from the title, introduction, and benefits to craft a conclusion that:\n1. Summarizes the key points discussed in the blog post.\n2. Reinforces the importance of morning exercise.\n3. Encourages readers to take action and start their morning exercise routine.\n4. Maintains a positive and motivating tone.\n\nEnsure the conclusion is concise, engaging, and leaves readers feeling inspired to make a change in their daily routine.\n\nFinally, write the conclusion paragraph that encourages readers to start their morning exercise routine.", - "input_vars_required": [], - "depends_on": [ - "BLOG_TITLE", - "INTRODUCTION", - "BENEFITS" - ] - }, - { - "subtask": "5. Compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post. -", - "tag": "FINAL_BLOG_POST", - "constraints": [ - { - "constraint": "Include a catchy title", - "validation_strategy": "llm" - }, - { - "constraint": "Include an introduction paragraph", - "validation_strategy": "llm" - }, - { - "constraint": "Include three main benefits with explanations", - "validation_strategy": "llm" - }, - { - "constraint": "Include a conclusion that encourages readers to start their morning exercise routine", - "validation_strategy": "llm" - } - ], - "prompt_template": "Your task is to compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post about the benefits of morning exercise.\n\nTo accomplish this, follow these steps:\n\n1. **Review the Components**:\n Carefully review the title, introduction, three main benefits, and conclusion that have been generated in the previous steps. These components are provided below:\n\n \n {{BLOG_TITLE}}\n \n\n \n {{INTRODUCTION}}\n \n\n \n {{BENEFITS}}\n \n\n \n {{CONCLUSION}}\n \n\n2. **Structure the Blog Post**:\n Organize the components into a well-structured blog post. The structure should include:\n - The catchy title at the beginning.\n - The introduction paragraph that sets the stage for the blog post.\n - The three main benefits with detailed explanations.\n - The conclusion that encourages readers to start their morning exercise routine.\n\n3. **Ensure Cohesion**:\n Make sure the blog post flows smoothly from one section to the next. The transitions between the introduction, benefits, and conclusion should be natural and logical.\n\n4. **Check for Consistency**:\n Verify that the tone and style are consistent throughout the blog post. Ensure that the language used in the title, introduction, benefits, and conclusion aligns with the overall theme of the blog post.\n\n5. **Final Review**:\n Read through the entire blog post to ensure it is cohesive, well-organized, and free of any grammatical or spelling errors. Make any necessary adjustments to improve clarity and readability.\n\n6. **Output the Blog Post**:\n Provide the final compiled blog post as your answer. Ensure that the output includes only the blog post text without any additional information or instructions.\n\nBy following these steps, you will create a single cohesive blog post that effectively communicates the benefits of morning exercise.", - "input_vars_required": [], - "depends_on": [ - "BLOG_TITLE", - "INTRODUCTION", - "BENEFITS", - "CONCLUSION" - ] - } - ] -} \ No newline at end of file diff --git a/docs/examples/m_decompose/python/python_decompose_result.py b/docs/examples/m_decompose/python/python_decompose_result.py deleted file mode 100644 index e4f77c9e3..000000000 --- a/docs/examples/m_decompose/python/python_decompose_result.py +++ /dev/null @@ -1,222 +0,0 @@ -# pytest: skip_always -import textwrap - -import mellea - -# Note: This is an example of an intermediary result from using decompose in python_decompose_example.py, not an example of how to use decompose. - - -m = mellea.start_session() - - -# 1. Create a catchy title for the blog post about the benefits of morning exercise. - - BLOG_TITLE -blog_title = m.instruct( - textwrap.dedent( - R""" - Your task is to create a catchy title for a blog post about the benefits of morning exercise. Follow these steps to accomplish your task: - - 1. **Understand the Topic**: The blog post will focus on the benefits of morning exercise. The title should be engaging and clearly convey the main topic of the post. - - 2. **Identify Key Elements**: Consider the key elements that make morning exercise beneficial. These could include improved mood, increased energy, better focus, and enhanced metabolism. - - 3. **Use Power Words**: Incorporate power words that evoke curiosity, excitement, or a sense of urgency. Examples include "Boost," "Transform," "Unlock," "Energize," and "Revitalize." - - 4. **Keep It Concise**: The title should be short and to the point, ideally between 5 to 10 words. It should be easy to read and remember. - - 5. **Make It Action-Oriented**: Use verbs that encourage action, such as "Start," "Jumpstart," "Kickstart," or "Ignite." - - 6. **Consider SEO**: Think about common search terms related to morning exercise. Including relevant keywords can help improve the post's visibility. - - 7. **Examples for Inspiration**: - - "Jumpstart Your Day: The Power of Morning Exercise" - - "Energize Your Mornings: Unlock the Benefits of Morning Exercise" - - "Transform Your Day with Morning Exercise" - - "Boost Your Energy: The Magic of Morning Workouts" - - "Revitalize Your Mornings: The Benefits of Morning Exercise" - - 8. **Create the Title**: Based on the above guidelines, create a catchy and engaging title for the blog post. Ensure it captures the essence of the topic and entices readers to click and read more. - - Your final answer should be only the title text. - """.strip() - ), - requirements=["Include a catchy title"], -) -assert blog_title.value is not None, 'ERROR: task "blog_title" execution failed' - -# 2. Write an introduction paragraph that sets the stage for the blog post. - - INTRODUCTION -introduction = m.instruct( - textwrap.dedent( - R""" - Your task is to write an engaging introduction paragraph for a blog post about the benefits of morning exercise. The introduction should set the stage for the blog post, capturing the reader's attention and providing a brief overview of what will be discussed. - - To accomplish this, follow these steps: - - 1. **Understand the Context**: - - The blog post is about the benefits of morning exercise. - - The title of the blog post is: {{BLOG_TITLE}} - - 2. **Craft the Introduction**: - - Start with a hook that grabs the reader's attention. This could be a question, a surprising fact, or a relatable scenario. - - Briefly introduce the topic of morning exercise and why it is important. - - Provide a smooth transition to the main benefits that will be discussed in the blog post. - - 3. **Ensure Engagement**: - - Use a conversational and engaging tone to connect with the readers. - - Keep the introduction concise and to the point, ideally between 3 to 5 sentences. - - Here is an example structure to guide your writing: - - **Sentence 1**: Hook to grab the reader's attention. - - **Sentence 2**: Introduce the topic of morning exercise. - - **Sentence 3**: Briefly mention the benefits that will be discussed. - - **Sentence 4**: Transition to the main content of the blog post. - - Ensure that the introduction flows naturally and sets the stage for the rest of the blog post. You should write only the introduction paragraph, do not include the guidance structure. - """.strip() - ), - requirements=["Include an introduction paragraph"], - user_variables={"BLOG_TITLE": blog_title.value}, -) -assert introduction.value is not None, 'ERROR: task "introduction" execution failed' - -# 3. Identify and explain three main benefits of morning exercise with detailed explanations. - - BENEFITS -benefits = m.instruct( - textwrap.dedent( - R""" - Your task is to identify and explain three main benefits of morning exercise with detailed explanations. Follow these steps to accomplish your task: - - First, review the title and introduction created in the previous steps to understand the context and tone of the blog post: - - {{BLOG_TITLE}} - - - {{INTRODUCTION}} - - - Next, research and identify three main benefits of morning exercise. These benefits should be supported by evidence or expert opinions to ensure credibility. - - For each benefit, provide a detailed explanation that includes: - - The specific benefit of morning exercise - - How this benefit positively impacts health, well-being, or daily life - - Any relevant studies, expert opinions, or personal anecdotes that support the benefit - - Ensure that the explanations are clear, concise, and engaging to keep the reader interested. - - Finally, present the three main benefits with their detailed explanations in a structured format that can be easily integrated into the blog post. - """.strip() - ), - requirements=["Include three main benefits with explanations"], - user_variables={"BLOG_TITLE": blog_title.value, "INTRODUCTION": introduction.value}, -) -assert benefits.value is not None, 'ERROR: task "benefits" execution failed' - -# 4. Write a conclusion that encourages readers to start their morning exercise routine. - - CONCLUSION -conclusion = m.instruct( - textwrap.dedent( - R""" - Your task is to write a compelling conclusion for a blog post about the benefits of morning exercise. The conclusion should encourage readers to start their morning exercise routine. Follow these steps to accomplish your task: - - First, review the title and introduction of the blog post to understand the context and tone: - - {{BLOG_TITLE}} - - - {{INTRODUCTION}} - - - Next, consider the three main benefits of morning exercise that have been previously identified and explained: - - {{BENEFITS}} - - - Use the information from the title, introduction, and benefits to craft a conclusion that: - 1. Summarizes the key points discussed in the blog post. - 2. Reinforces the importance of morning exercise. - 3. Encourages readers to take action and start their morning exercise routine. - 4. Maintains a positive and motivating tone. - - Ensure the conclusion is concise, engaging, and leaves readers feeling inspired to make a change in their daily routine. - - Finally, write the conclusion paragraph that encourages readers to start their morning exercise routine. - """.strip() - ), - requirements=[ - "Include a conclusion that encourages readers to start their morning exercise routine" - ], - user_variables={ - "BLOG_TITLE": blog_title.value, - "INTRODUCTION": introduction.value, - "BENEFITS": benefits.value, - }, -) -assert conclusion.value is not None, 'ERROR: task "conclusion" execution failed' - -# 5. Compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post. - - FINAL_BLOG_POST -final_blog_post = m.instruct( - textwrap.dedent( - R""" - Your task is to compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post about the benefits of morning exercise. - - To accomplish this, follow these steps: - - 1. **Review the Components**: - Carefully review the title, introduction, three main benefits, and conclusion that have been generated in the previous steps. These components are provided below: - - - {{BLOG_TITLE}} - - - - {{INTRODUCTION}} - - - - {{BENEFITS}} - - - - {{CONCLUSION}} - - - 2. **Structure the Blog Post**: - Organize the components into a well-structured blog post. The structure should include: - - The catchy title at the beginning. - - The introduction paragraph that sets the stage for the blog post. - - The three main benefits with detailed explanations. - - The conclusion that encourages readers to start their morning exercise routine. - - 3. **Ensure Cohesion**: - Make sure the blog post flows smoothly from one section to the next. The transitions between the introduction, benefits, and conclusion should be natural and logical. - - 4. **Check for Consistency**: - Verify that the tone and style are consistent throughout the blog post. Ensure that the language used in the title, introduction, benefits, and conclusion aligns with the overall theme of the blog post. - - 5. **Final Review**: - Read through the entire blog post to ensure it is cohesive, well-organized, and free of any grammatical or spelling errors. Make any necessary adjustments to improve clarity and readability. - - 6. **Output the Blog Post**: - Provide the final compiled blog post as your answer. Ensure that the output includes only the blog post text without any additional information or instructions. - - By following these steps, you will create a single cohesive blog post that effectively communicates the benefits of morning exercise. - """.strip() - ), - requirements=[ - "Include a catchy title", - "Include an introduction paragraph", - "Include three main benefits with explanations", - "Include a conclusion that encourages readers to start their morning exercise routine", - ], - user_variables={ - "BLOG_TITLE": blog_title.value, - "INTRODUCTION": introduction.value, - "BENEFITS": benefits.value, - "CONCLUSION": conclusion.value, - }, -) -assert final_blog_post.value is not None, ( - 'ERROR: task "final_blog_post" execution failed' -) - - -final_answer = final_blog_post.value - -print(final_answer) From 985a75f403db9a75f24e21804b58943bb8e311a7 Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 31 Mar 2026 23:30:46 -0400 Subject: [PATCH 07/26] upd: clean example --- docs/examples/m_decompose/example.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/m_decompose/example.txt b/docs/examples/m_decompose/example.txt index f6c2f4af5..f536187b2 100644 --- a/docs/examples/m_decompose/example.txt +++ b/docs/examples/m_decompose/example.txt @@ -1,3 +1,3 @@ -I will visit Grand Canyon National Park for 3 days in early May. Please create a travel itinerary that includes major scenic viewpoints and short hiking trails. The daily walking distance should stay under 6 miles, and each day should include at least one sunset or sunrise viewpoint. +I will visit Grand Canyon National Park for 3 days in early May. Please create a travel itinerary that includes major scenic viewpoints and short hiking trails. The daily walking distance should stay under 6 miles, and at least one day should include at least one sunset or sunrise viewpoint. I am preparing my U.S. federal tax return for 2025. My income sources include W-2 salary, house rental, and investment dividends. Please help estimate my federal tax liability and suitable which types of program of TurboTax for possible deductions. Assume I can take the standard/itemized deduction, and the calculation should assume I live in Virginia. We are building an AI assistant for meeting productivity that can summarize meetings and generate follow-up tasks. Please propose 5 core product features that would provide the most value to users. The features should be feasible and simple enough for a MVP, and each feature should be under 60 words. \ No newline at end of file From 0376fae72eb515efd4a9d9da431c0e1bc9ad64e8 Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 14:40:52 -0400 Subject: [PATCH 08/26] upd: setup default model --- docs/examples/m_decompose/decompose_using_cli.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/m_decompose/decompose_using_cli.sh b/docs/examples/m_decompose/decompose_using_cli.sh index 783a80740..52b1fe275 100755 --- a/docs/examples/m_decompose/decompose_using_cli.sh +++ b/docs/examples/m_decompose/decompose_using_cli.sh @@ -1,7 +1,7 @@ -#MODEL_ID=granite3.3 -#MODEL_ID=granite4 +#MODEL_ID=granite3.3:latest +#MODEL_ID=granite4:latest -MODEL_ID=mistral-small3.2 #qwen2.5:7b +MODEL_ID=mistral-small3.2:latest #granite4:latest m decompose run --model-id $MODEL_ID --out-dir ./ --input-file example.txt From cad3c659732c6d1aad3f005e0152aa5e6833c374 Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 14:54:11 -0400 Subject: [PATCH 09/26] clean: pre-commit --- cli/decompose/decompose.py | 2 +- .../m_decompose/decompose_using_cli.sh | 1 - .../python_decompose_example.py\342\200\216" | 228 ++++++++++++++++++ 3 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 "docs/examples/m_decompose/python/python_decompose_example.py\342\200\216" diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 4b6f80302..03467a2a9 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -410,4 +410,4 @@ def run( for decomp_dir in reversed(created_dirs): if decomp_dir.exists() and decomp_dir.is_dir(): shutil.rmtree(decomp_dir) - raise + raise \ No newline at end of file diff --git a/docs/examples/m_decompose/decompose_using_cli.sh b/docs/examples/m_decompose/decompose_using_cli.sh index 52b1fe275..eab099666 100755 --- a/docs/examples/m_decompose/decompose_using_cli.sh +++ b/docs/examples/m_decompose/decompose_using_cli.sh @@ -4,4 +4,3 @@ MODEL_ID=mistral-small3.2:latest #granite4:latest m decompose run --model-id $MODEL_ID --out-dir ./ --input-file example.txt - diff --git "a/docs/examples/m_decompose/python/python_decompose_example.py\342\200\216" "b/docs/examples/m_decompose/python/python_decompose_example.py\342\200\216" new file mode 100644 index 000000000..5bdc70b14 --- /dev/null +++ "b/docs/examples/m_decompose/python/python_decompose_example.py\342\200\216" @@ -0,0 +1,228 @@ +# pytest: ollama, llm, slow, qualitative +#!/usr/bin/env python3 +""" +Example: Using Mellea's decompose functionality programmatically + +This script demonstrates how to use the decompose pipeline from Python code +to break down a complex task into subtasks with generated prompts. +""" + +import json +import subprocess +import textwrap +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + +# Import the decompose pipeline from the CLI module +from cli.decompose.pipeline import DecompBackend, DecompPipelineResult, decompose + + +def run_decompose(task_prompt: str) -> DecompPipelineResult: + """ + Run the decompose pipeline on a task prompt. + + Args: + task_prompt: The task description to decompose + + Returns: + Dictionary containing decomposition results + """ + print("Running decomposition pipeline...\n") + + result = decompose( + task_prompt=task_prompt, + model_id="granite3.3:8b", # Note micro will not properly create tags, need 8b + backend=DecompBackend.ollama, # Use Ollama backend + backend_req_timeout=300, # 5 minute timeout + ) + + return result + + +def save_decompose_json( + result: DecompPipelineResult, + output_dir: Path, + filename: str = "python_decompose_result.json", +) -> Path: + """ + Save decomposition results to a JSON file. + + Args: + result: Decomposition results dictionary + output_dir: Directory to save the file + filename: Name of the output file + + Returns: + Path to the saved JSON file + """ + json_output_file = output_dir / filename + + with open(json_output_file, "w") as f: + json.dump(result, f, indent=2) + + print(f"💾 JSON results saved to: {json_output_file}") + return json_output_file + + +def generate_python_script( + result: DecompPipelineResult, + output_dir: Path, + filename: str = "python_decompose_result.py", +) -> Path: + """ + Generate an executable Python script from decomposition results. + + Args: + result: Decomposition results dictionary + output_dir: Directory to save the file + filename: Name of the output Python file + + Returns: + Path to the generated Python script + """ + print("\n📝 Generating executable Python script...") + + # Load the template from the CLI decompose directory + cli_decompose_dir = ( + Path(__file__).parent.parent.parent.parent.parent / "cli" / "decompose" + ) + environment = Environment( + loader=FileSystemLoader(cli_decompose_dir), autoescape=False + ) + m_template = environment.get_template("m_decomp_result_v1.py.jinja2") + + # Render the template with the decomposition results + python_script_content = m_template.render( + subtasks=result["subtasks"], + user_inputs=[], # No user inputs for this simple example + ) + + # Save the generated Python script + py_output_file = output_dir / filename + with open(py_output_file, "w") as f: + f.write(python_script_content + "\n") + + print(f"💾 Generated Python script saved to: {py_output_file}") + return py_output_file + + +def run_generated_script( + script_path: Path, output_dir: Path, timeout: int = 600 +) -> Path | None: + """ + Execute the generated Python script to produce final output. + + Args: + script_path: Path to the Python script to execute + output_dir: Directory to save the final output + timeout: Maximum execution time in seconds + + Returns: + Path to the final output file if successful, None otherwise + """ + print("\n🚀 Running the generated script to produce final output...") + print(" (This may take a few minutes as it calls the LLM for each subtask)") + + try: + result_output = subprocess.run( + ["python3", str(script_path)], + capture_output=True, + text=True, + timeout=timeout, + cwd=output_dir, + ) + + if result_output.returncode == 0: + # Save the final output + final_output_file = output_dir / "python_decompose_final_output.txt" + with open(final_output_file, "w") as f: + f.write(result_output.stdout) + + print(f"✅ Final output saved to: {final_output_file}") + print("\n" + "=" * 70) + print("Final Output:") + print("=" * 70) + preview = result_output.stdout + print(preview) + return final_output_file + else: + print( + f"❌ Script execution failed with return code {result_output.returncode}" + ) + print(f"Error: {result_output.stderr}") + return None + except subprocess.TimeoutExpired: + print(f"⏱️ Script execution timed out after {timeout} seconds") + return None + except Exception as e: + print(f"❌ Error running script: {e}") + return None + + +def display_results(result: DecompPipelineResult): + """ + Display decomposition results in a formatted way. + + Args: + result: Decomposition results dictionary + """ + print("=" * 70) + print("Decomposition Results") + print("=" * 70) + + print(f"\n📋 Subtasks Identified ({len(result['subtask_list'])}):") + for i, subtask in enumerate(result["subtask_list"], 1): + print(f" {subtask}") + + print(f"\n🔍 Constraints Identified ({len(result['identified_constraints'])}):") + for i, constraint in enumerate(result["identified_constraints"], 1): + print(f" {i}. {constraint['constraint']}") + print(f" Validation: {constraint['val_strategy']}") + + print(f"\n🎯 Detailed Subtasks ({len(result['subtasks'])}):") + for i, subtask_detail in enumerate(result["subtasks"], 1): + print(f"\n Subtask {subtask_detail['subtask']}") + print(f" Tag: {subtask_detail['tag']}") + print(f" Dependencies: {subtask_detail['depends_on'] or 'None'}") + print(f" Input Variables: {subtask_detail['input_vars_required'] or 'None'}") + print(f" Constraints: {len(subtask_detail['constraints'])}") + + +def main(): + # Define a simple task prompt to decompose + task_prompt = textwrap.dedent(""" + Write a short blog post about the benefits of morning exercise. + Include a catchy title, an introduction paragraph, three main benefits + with explanations, and a conclusion that encourages readers to start + their morning exercise routine. + """).strip() + + print("=" * 70) + print("Mellea Decompose Example") + print("=" * 70) + print(f"\nOriginal Task:\n\n{task_prompt.strip()}\n") + + # Step 1: Run decomposition + result = run_decompose(task_prompt) + + # Step 2: Display results + display_results(result) + + # Step 3: Save JSON results + output_dir = Path(__file__).parent + save_decompose_json(result, output_dir) + + # Step 4: Generate Python script + script_path = generate_python_script(result, output_dir) + + # Step 5: Run the generated script (optional) + run_generated_script(script_path, output_dir) + + print("\n" + "=" * 70) + print("✅ Decomposition complete!") + print("=" * 70) + + +if __name__ == "__main__": + main() \ No newline at end of file From 415fef5976766b978d4e2929d062346e295a1553 Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 14:55:47 -0400 Subject: [PATCH 10/26] clean: tmp file --- .../python_decompose_example.py\342\200\216" | 228 ------------------ 1 file changed, 228 deletions(-) delete mode 100644 "docs/examples/m_decompose/python/python_decompose_example.py\342\200\216" diff --git "a/docs/examples/m_decompose/python/python_decompose_example.py\342\200\216" "b/docs/examples/m_decompose/python/python_decompose_example.py\342\200\216" deleted file mode 100644 index 5bdc70b14..000000000 --- "a/docs/examples/m_decompose/python/python_decompose_example.py\342\200\216" +++ /dev/null @@ -1,228 +0,0 @@ -# pytest: ollama, llm, slow, qualitative -#!/usr/bin/env python3 -""" -Example: Using Mellea's decompose functionality programmatically - -This script demonstrates how to use the decompose pipeline from Python code -to break down a complex task into subtasks with generated prompts. -""" - -import json -import subprocess -import textwrap -from pathlib import Path - -from jinja2 import Environment, FileSystemLoader - -# Import the decompose pipeline from the CLI module -from cli.decompose.pipeline import DecompBackend, DecompPipelineResult, decompose - - -def run_decompose(task_prompt: str) -> DecompPipelineResult: - """ - Run the decompose pipeline on a task prompt. - - Args: - task_prompt: The task description to decompose - - Returns: - Dictionary containing decomposition results - """ - print("Running decomposition pipeline...\n") - - result = decompose( - task_prompt=task_prompt, - model_id="granite3.3:8b", # Note micro will not properly create tags, need 8b - backend=DecompBackend.ollama, # Use Ollama backend - backend_req_timeout=300, # 5 minute timeout - ) - - return result - - -def save_decompose_json( - result: DecompPipelineResult, - output_dir: Path, - filename: str = "python_decompose_result.json", -) -> Path: - """ - Save decomposition results to a JSON file. - - Args: - result: Decomposition results dictionary - output_dir: Directory to save the file - filename: Name of the output file - - Returns: - Path to the saved JSON file - """ - json_output_file = output_dir / filename - - with open(json_output_file, "w") as f: - json.dump(result, f, indent=2) - - print(f"💾 JSON results saved to: {json_output_file}") - return json_output_file - - -def generate_python_script( - result: DecompPipelineResult, - output_dir: Path, - filename: str = "python_decompose_result.py", -) -> Path: - """ - Generate an executable Python script from decomposition results. - - Args: - result: Decomposition results dictionary - output_dir: Directory to save the file - filename: Name of the output Python file - - Returns: - Path to the generated Python script - """ - print("\n📝 Generating executable Python script...") - - # Load the template from the CLI decompose directory - cli_decompose_dir = ( - Path(__file__).parent.parent.parent.parent.parent / "cli" / "decompose" - ) - environment = Environment( - loader=FileSystemLoader(cli_decompose_dir), autoescape=False - ) - m_template = environment.get_template("m_decomp_result_v1.py.jinja2") - - # Render the template with the decomposition results - python_script_content = m_template.render( - subtasks=result["subtasks"], - user_inputs=[], # No user inputs for this simple example - ) - - # Save the generated Python script - py_output_file = output_dir / filename - with open(py_output_file, "w") as f: - f.write(python_script_content + "\n") - - print(f"💾 Generated Python script saved to: {py_output_file}") - return py_output_file - - -def run_generated_script( - script_path: Path, output_dir: Path, timeout: int = 600 -) -> Path | None: - """ - Execute the generated Python script to produce final output. - - Args: - script_path: Path to the Python script to execute - output_dir: Directory to save the final output - timeout: Maximum execution time in seconds - - Returns: - Path to the final output file if successful, None otherwise - """ - print("\n🚀 Running the generated script to produce final output...") - print(" (This may take a few minutes as it calls the LLM for each subtask)") - - try: - result_output = subprocess.run( - ["python3", str(script_path)], - capture_output=True, - text=True, - timeout=timeout, - cwd=output_dir, - ) - - if result_output.returncode == 0: - # Save the final output - final_output_file = output_dir / "python_decompose_final_output.txt" - with open(final_output_file, "w") as f: - f.write(result_output.stdout) - - print(f"✅ Final output saved to: {final_output_file}") - print("\n" + "=" * 70) - print("Final Output:") - print("=" * 70) - preview = result_output.stdout - print(preview) - return final_output_file - else: - print( - f"❌ Script execution failed with return code {result_output.returncode}" - ) - print(f"Error: {result_output.stderr}") - return None - except subprocess.TimeoutExpired: - print(f"⏱️ Script execution timed out after {timeout} seconds") - return None - except Exception as e: - print(f"❌ Error running script: {e}") - return None - - -def display_results(result: DecompPipelineResult): - """ - Display decomposition results in a formatted way. - - Args: - result: Decomposition results dictionary - """ - print("=" * 70) - print("Decomposition Results") - print("=" * 70) - - print(f"\n📋 Subtasks Identified ({len(result['subtask_list'])}):") - for i, subtask in enumerate(result["subtask_list"], 1): - print(f" {subtask}") - - print(f"\n🔍 Constraints Identified ({len(result['identified_constraints'])}):") - for i, constraint in enumerate(result["identified_constraints"], 1): - print(f" {i}. {constraint['constraint']}") - print(f" Validation: {constraint['val_strategy']}") - - print(f"\n🎯 Detailed Subtasks ({len(result['subtasks'])}):") - for i, subtask_detail in enumerate(result["subtasks"], 1): - print(f"\n Subtask {subtask_detail['subtask']}") - print(f" Tag: {subtask_detail['tag']}") - print(f" Dependencies: {subtask_detail['depends_on'] or 'None'}") - print(f" Input Variables: {subtask_detail['input_vars_required'] or 'None'}") - print(f" Constraints: {len(subtask_detail['constraints'])}") - - -def main(): - # Define a simple task prompt to decompose - task_prompt = textwrap.dedent(""" - Write a short blog post about the benefits of morning exercise. - Include a catchy title, an introduction paragraph, three main benefits - with explanations, and a conclusion that encourages readers to start - their morning exercise routine. - """).strip() - - print("=" * 70) - print("Mellea Decompose Example") - print("=" * 70) - print(f"\nOriginal Task:\n\n{task_prompt.strip()}\n") - - # Step 1: Run decomposition - result = run_decompose(task_prompt) - - # Step 2: Display results - display_results(result) - - # Step 3: Save JSON results - output_dir = Path(__file__).parent - save_decompose_json(result, output_dir) - - # Step 4: Generate Python script - script_path = generate_python_script(result, output_dir) - - # Step 5: Run the generated script (optional) - run_generated_script(script_path, output_dir) - - print("\n" + "=" * 70) - print("✅ Decomposition complete!") - print("=" * 70) - - -if __name__ == "__main__": - main() \ No newline at end of file From fb45a6dcc56bec70f3024eafe731b8a6397cc1a5 Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 18:35:18 -0400 Subject: [PATCH 11/26] sync: revert to the main --- cli/decompose/decompose.py | 2 +- docs/examples/m_decompose/decompose_using_cli.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 03467a2a9..4b6f80302 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -410,4 +410,4 @@ def run( for decomp_dir in reversed(created_dirs): if decomp_dir.exists() and decomp_dir.is_dir(): shutil.rmtree(decomp_dir) - raise \ No newline at end of file + raise diff --git a/docs/examples/m_decompose/decompose_using_cli.sh b/docs/examples/m_decompose/decompose_using_cli.sh index eab099666..56eb07102 100755 --- a/docs/examples/m_decompose/decompose_using_cli.sh +++ b/docs/examples/m_decompose/decompose_using_cli.sh @@ -1,6 +1,6 @@ #MODEL_ID=granite3.3:latest #MODEL_ID=granite4:latest -MODEL_ID=mistral-small3.2:latest #granite4:latest +MODEL_ID=mistral-small3.2:latest # granite4:latest m decompose run --model-id $MODEL_ID --out-dir ./ --input-file example.txt From 131d0d93e90894629b453b60068e88f8c6c35d1d Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 18:38:00 -0400 Subject: [PATCH 12/26] sync: revert to the main --- cli/decompose/decompose.py | 82 ++++--- .../python/python_decompose_result.py | 222 ++++++++++++++++++ 2 files changed, 262 insertions(+), 42 deletions(-) create mode 100644 docs/examples/m_decompose/python/python_decompose_result.py diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 4b6f80302..702cb5f99 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -27,22 +27,22 @@ class DecompVersion(StrEnum): - """Available versions of the decomposition pipeline template. + """Available template versions for generated decomposition programs. - Newer versions must be declared last to ensure ``latest`` always resolves to - the most recent template. + Newer concrete versions must be declared after older ones so that + ``latest`` can resolve to the most recently declared template version. Attributes: - latest (str): Sentinel value that resolves to the last declared version. - v1 (str): Version 1 of the decomposition pipeline template. - v2 (str): Version 2 of the decomposition pipeline template. - v3 (str): Version 3 of the decomposition pipeline template. + latest: Sentinel value that resolves to the last declared concrete + template version. + v1: Version 1 of the decomposition program template. + v2: Version 2 of the decomposition program template. """ latest = "latest" v1 = "v1" v2 = "v2" - v3 = "v3" + # v3 = "v3" this_file_dir = Path(__file__).resolve().parent @@ -227,44 +227,42 @@ def run( ), ] = LogMode.demo, ) -> None: - """Decompose one or more user queries into subtasks with constraints and dependency metadata. + """Runs the ``m decompose`` CLI workflow and writes generated outputs. - Reads user queries either from a file or interactively, runs the LLM - decomposition pipeline to produce subtask descriptions, Jinja2 prompt templates, - constraint lists, and dependency metadata, and writes one ``.json`` result file - plus one rendered ``.py`` script per task job to the output directory. - - If ``input_file`` contains multiple non-empty lines, each line is treated as a - separate task job. + Reads user queries from a file or interactive input, runs the decomposition + pipeline for each task job, and writes one JSON file, one rendered Python + program, and any generated validation modules under a per-job output + directory. Args: - out_dir: Path to an existing directory where output files are saved. - out_name: Base name (no extension) for the output files. Defaults to - ``"m_decomp_result"``. - input_file: Optional path to a text file containing one or more user - queries. If the file contains multiple non-empty lines, each line is - treated as a separate task job. If omitted, the query is collected - interactively. - model_id: Model name or ID used for all decomposition pipeline steps. - backend: Inference backend -- ``"ollama"``, ``"openai"``, or ``"rits"``. - backend_req_timeout: Request timeout in seconds for model inference calls. - backend_endpoint: Base URL of the configured endpoint. Required when - ``backend="openai"`` or ``backend="rits"``. - backend_api_key: API key for the configured endpoint. Required when - ``backend="openai"`` or ``backend="rits"``. - version: Version of the decomposition pipeline template to use. - input_var: Optional list of user-input variable names (e.g. ``"DOC"``). - Each name must be a valid Python identifier. Pass this option - multiple times to define multiple variables. - log_mode: Logging detail mode for CLI and pipeline output. + out_dir: Existing directory under which per-job output directories are + created. + out_name: Base name used for the per-job output directory and generated + files. + input_file: Optional path to a text file containing one or more task + prompts. Each non-empty line is processed as a separate task job. + When omitted, the command prompts interactively for one task. + model_id: Model identifier used for all decomposition pipeline stages. + backend: Inference backend used to execute model calls. + backend_req_timeout: Request timeout in seconds for backend inference calls. + backend_endpoint: Endpoint URL or base URL required by remote backends. + backend_api_key: API key required by remote backends. + version: Template version used to render the generated Python program. + ``latest`` resolves to the most recently declared concrete version. + input_var: Optional user input variable names to expose in generated + prompts and programs. Each name must be a valid non-keyword Python + identifier. + log_mode: Logging verbosity for CLI and pipeline execution. Raises: - AssertionError: If ``out_name`` contains invalid characters, if - ``out_dir`` does not exist or is not a directory, or if any - ``input_var`` name is not a valid Python identifier. - ValueError: If the input file contains no non-empty task lines. - Exception: Re-raised from the decomposition pipeline after cleaning up - any partially written output directories. + AssertionError: If ``out_name`` is invalid, ``out_dir`` does not name an + existing directory, ``input_file`` does not name an existing file, + or any declared ``input_var`` is not a valid Python identifier. + ValueError: If ``input_file`` exists but contains no non-empty task + lines. + Exception: Propagates pipeline, rendering, parsing, or file-writing + failures. Any output directories created earlier in the run are + removed before the exception is re-raised. """ created_dirs: list[Path] = [] @@ -410,4 +408,4 @@ def run( for decomp_dir in reversed(created_dirs): if decomp_dir.exists() and decomp_dir.is_dir(): shutil.rmtree(decomp_dir) - raise + raise \ No newline at end of file diff --git a/docs/examples/m_decompose/python/python_decompose_result.py b/docs/examples/m_decompose/python/python_decompose_result.py new file mode 100644 index 000000000..5c52667aa --- /dev/null +++ b/docs/examples/m_decompose/python/python_decompose_result.py @@ -0,0 +1,222 @@ +# pytest: skip_always +import textwrap + +import mellea + +# Note: This is an example of an intermediary result from using decompose in python_decompose_example.py, not an example of how to use decompose. + + +m = mellea.start_session() + + +# 1. Create a catchy title for the blog post about the benefits of morning exercise. - - BLOG_TITLE +blog_title = m.instruct( + textwrap.dedent( + R""" + Your task is to create a catchy title for a blog post about the benefits of morning exercise. Follow these steps to accomplish your task: + + 1. **Understand the Topic**: The blog post will focus on the benefits of morning exercise. The title should be engaging and clearly convey the main topic of the post. + + 2. **Identify Key Elements**: Consider the key elements that make morning exercise beneficial. These could include improved mood, increased energy, better focus, and enhanced metabolism. + + 3. **Use Power Words**: Incorporate power words that evoke curiosity, excitement, or a sense of urgency. Examples include "Boost," "Transform," "Unlock," "Energize," and "Revitalize." + + 4. **Keep It Concise**: The title should be short and to the point, ideally between 5 to 10 words. It should be easy to read and remember. + + 5. **Make It Action-Oriented**: Use verbs that encourage action, such as "Start," "Jumpstart," "Kickstart," or "Ignite." + + 6. **Consider SEO**: Think about common search terms related to morning exercise. Including relevant keywords can help improve the post's visibility. + + 7. **Examples for Inspiration**: + - "Jumpstart Your Day: The Power of Morning Exercise" + - "Energize Your Mornings: Unlock the Benefits of Morning Exercise" + - "Transform Your Day with Morning Exercise" + - "Boost Your Energy: The Magic of Morning Workouts" + - "Revitalize Your Mornings: The Benefits of Morning Exercise" + + 8. **Create the Title**: Based on the above guidelines, create a catchy and engaging title for the blog post. Ensure it captures the essence of the topic and entices readers to click and read more. + + Your final answer should be only the title text. + """.strip() + ), + requirements=["Include a catchy title"], +) +assert blog_title.value is not None, 'ERROR: task "blog_title" execution failed' + +# 2. Write an introduction paragraph that sets the stage for the blog post. - - INTRODUCTION +introduction = m.instruct( + textwrap.dedent( + R""" + Your task is to write an engaging introduction paragraph for a blog post about the benefits of morning exercise. The introduction should set the stage for the blog post, capturing the reader's attention and providing a brief overview of what will be discussed. + + To accomplish this, follow these steps: + + 1. **Understand the Context**: + - The blog post is about the benefits of morning exercise. + - The title of the blog post is: {{BLOG_TITLE}} + + 2. **Craft the Introduction**: + - Start with a hook that grabs the reader's attention. This could be a question, a surprising fact, or a relatable scenario. + - Briefly introduce the topic of morning exercise and why it is important. + - Provide a smooth transition to the main benefits that will be discussed in the blog post. + + 3. **Ensure Engagement**: + - Use a conversational and engaging tone to connect with the readers. + - Keep the introduction concise and to the point, ideally between 3 to 5 sentences. + + Here is an example structure to guide your writing: + - **Sentence 1**: Hook to grab the reader's attention. + - **Sentence 2**: Introduce the topic of morning exercise. + - **Sentence 3**: Briefly mention the benefits that will be discussed. + - **Sentence 4**: Transition to the main content of the blog post. + + Ensure that the introduction flows naturally and sets the stage for the rest of the blog post. You should write only the introduction paragraph, do not include the guidance structure. + """.strip() + ), + requirements=["Include an introduction paragraph"], + user_variables={"BLOG_TITLE": blog_title.value}, +) +assert introduction.value is not None, 'ERROR: task "introduction" execution failed' + +# 3. Identify and explain three main benefits of morning exercise with detailed explanations. - - BENEFITS +benefits = m.instruct( + textwrap.dedent( + R""" + Your task is to identify and explain three main benefits of morning exercise with detailed explanations. Follow these steps to accomplish your task: + + First, review the title and introduction created in the previous steps to understand the context and tone of the blog post: + + {{BLOG_TITLE}} + + + {{INTRODUCTION}} + + + Next, research and identify three main benefits of morning exercise. These benefits should be supported by evidence or expert opinions to ensure credibility. + + For each benefit, provide a detailed explanation that includes: + - The specific benefit of morning exercise + - How this benefit positively impacts health, well-being, or daily life + - Any relevant studies, expert opinions, or personal anecdotes that support the benefit + + Ensure that the explanations are clear, concise, and engaging to keep the reader interested. + + Finally, present the three main benefits with their detailed explanations in a structured format that can be easily integrated into the blog post. + """.strip() + ), + requirements=["Include three main benefits with explanations"], + user_variables={"BLOG_TITLE": blog_title.value, "INTRODUCTION": introduction.value}, +) +assert benefits.value is not None, 'ERROR: task "benefits" execution failed' + +# 4. Write a conclusion that encourages readers to start their morning exercise routine. - - CONCLUSION +conclusion = m.instruct( + textwrap.dedent( + R""" + Your task is to write a compelling conclusion for a blog post about the benefits of morning exercise. The conclusion should encourage readers to start their morning exercise routine. Follow these steps to accomplish your task: + + First, review the title and introduction of the blog post to understand the context and tone: + + {{BLOG_TITLE}} + + + {{INTRODUCTION}} + + + Next, consider the three main benefits of morning exercise that have been previously identified and explained: + + {{BENEFITS}} + + + Use the information from the title, introduction, and benefits to craft a conclusion that: + 1. Summarizes the key points discussed in the blog post. + 2. Reinforces the importance of morning exercise. + 3. Encourages readers to take action and start their morning exercise routine. + 4. Maintains a positive and motivating tone. + + Ensure the conclusion is concise, engaging, and leaves readers feeling inspired to make a change in their daily routine. + + Finally, write the conclusion paragraph that encourages readers to start their morning exercise routine. + """.strip() + ), + requirements=[ + "Include a conclusion that encourages readers to start their morning exercise routine" + ], + user_variables={ + "BLOG_TITLE": blog_title.value, + "INTRODUCTION": introduction.value, + "BENEFITS": benefits.value, + }, +) +assert conclusion.value is not None, 'ERROR: task "conclusion" execution failed' + +# 5. Compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post. - - FINAL_BLOG_POST +final_blog_post = m.instruct( + textwrap.dedent( + R""" + Your task is to compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post about the benefits of morning exercise. + + To accomplish this, follow these steps: + + 1. **Review the Components**: + Carefully review the title, introduction, three main benefits, and conclusion that have been generated in the previous steps. These components are provided below: + + + {{BLOG_TITLE}} + + + + {{INTRODUCTION}} + + + + {{BENEFITS}} + + + + {{CONCLUSION}} + + + 2. **Structure the Blog Post**: + Organize the components into a well-structured blog post. The structure should include: + - The catchy title at the beginning. + - The introduction paragraph that sets the stage for the blog post. + - The three main benefits with detailed explanations. + - The conclusion that encourages readers to start their morning exercise routine. + + 3. **Ensure Cohesion**: + Make sure the blog post flows smoothly from one section to the next. The transitions between the introduction, benefits, and conclusion should be natural and logical. + + 4. **Check for Consistency**: + Verify that the tone and style are consistent throughout the blog post. Ensure that the language used in the title, introduction, benefits, and conclusion aligns with the overall theme of the blog post. + + 5. **Final Review**: + Read through the entire blog post to ensure it is cohesive, well-organized, and free of any grammatical or spelling errors. Make any necessary adjustments to improve clarity and readability. + + 6. **Output the Blog Post**: + Provide the final compiled blog post as your answer. Ensure that the output includes only the blog post text without any additional information or instructions. + + By following these steps, you will create a single cohesive blog post that effectively communicates the benefits of morning exercise. + """.strip() + ), + requirements=[ + "Include a catchy title", + "Include an introduction paragraph", + "Include three main benefits with explanations", + "Include a conclusion that encourages readers to start their morning exercise routine", + ], + user_variables={ + "BLOG_TITLE": blog_title.value, + "INTRODUCTION": introduction.value, + "BENEFITS": benefits.value, + "CONCLUSION": conclusion.value, + }, +) +assert final_blog_post.value is not None, ( + 'ERROR: task "final_blog_post" execution failed' +) + + +final_answer = final_blog_post.value + +print(final_answer) \ No newline at end of file From dd7a58eb1c1959bb1e6d009d41b2a27e857dc6a5 Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 18:40:41 -0400 Subject: [PATCH 13/26] sync: revert to the main --- .../python/python_decompose_example.py | 228 ++++++++++++++++++ .../python/python_decompose_result.py | 222 ----------------- 2 files changed, 228 insertions(+), 222 deletions(-) create mode 100644 docs/examples/m_decompose/python/python_decompose_example.py delete mode 100644 docs/examples/m_decompose/python/python_decompose_result.py diff --git a/docs/examples/m_decompose/python/python_decompose_example.py b/docs/examples/m_decompose/python/python_decompose_example.py new file mode 100644 index 000000000..701b414ea --- /dev/null +++ b/docs/examples/m_decompose/python/python_decompose_example.py @@ -0,0 +1,228 @@ +# pytest: ollama, e2e, slow, qualitative +#!/usr/bin/env python3 +""" +Example: Using Mellea's decompose functionality programmatically + +This script demonstrates how to use the decompose pipeline from Python code +to break down a complex task into subtasks with generated prompts. +""" + +import json +import subprocess +import textwrap +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + +# Import the decompose pipeline from the CLI module +from cli.decompose.pipeline import DecompBackend, DecompPipelineResult, decompose + + +def run_decompose(task_prompt: str) -> DecompPipelineResult: + """ + Run the decompose pipeline on a task prompt. + + Args: + task_prompt: The task description to decompose + + Returns: + Dictionary containing decomposition results + """ + print("Running decomposition pipeline...\n") + + result = decompose( + task_prompt=task_prompt, + model_id="granite3.3:8b", # Note micro will not properly create tags, need 8b + backend=DecompBackend.ollama, # Use Ollama backend + backend_req_timeout=300, # 5 minute timeout + ) + + return result + + +def save_decompose_json( + result: DecompPipelineResult, + output_dir: Path, + filename: str = "python_decompose_result.json", +) -> Path: + """ + Save decomposition results to a JSON file. + + Args: + result: Decomposition results dictionary + output_dir: Directory to save the file + filename: Name of the output file + + Returns: + Path to the saved JSON file + """ + json_output_file = output_dir / filename + + with open(json_output_file, "w") as f: + json.dump(result, f, indent=2) + + print(f"💾 JSON results saved to: {json_output_file}") + return json_output_file + + +def generate_python_script( + result: DecompPipelineResult, + output_dir: Path, + filename: str = "python_decompose_result.py", +) -> Path: + """ + Generate an executable Python script from decomposition results. + + Args: + result: Decomposition results dictionary + output_dir: Directory to save the file + filename: Name of the output Python file + + Returns: + Path to the generated Python script + """ + print("\n📝 Generating executable Python script...") + + # Load the template from the CLI decompose directory + cli_decompose_dir = ( + Path(__file__).parent.parent.parent.parent.parent / "cli" / "decompose" + ) + environment = Environment( + loader=FileSystemLoader(cli_decompose_dir), autoescape=False + ) + m_template = environment.get_template("m_decomp_result_v1.py.jinja2") + + # Render the template with the decomposition results + python_script_content = m_template.render( + subtasks=result["subtasks"], + user_inputs=[], # No user inputs for this simple example + ) + + # Save the generated Python script + py_output_file = output_dir / filename + with open(py_output_file, "w") as f: + f.write(python_script_content + "\n") + + print(f"💾 Generated Python script saved to: {py_output_file}") + return py_output_file + + +def run_generated_script( + script_path: Path, output_dir: Path, timeout: int = 600 +) -> Path | None: + """ + Execute the generated Python script to produce final output. + + Args: + script_path: Path to the Python script to execute + output_dir: Directory to save the final output + timeout: Maximum execution time in seconds + + Returns: + Path to the final output file if successful, None otherwise + """ + print("\n🚀 Running the generated script to produce final output...") + print(" (This may take a few minutes as it calls the LLM for each subtask)") + + try: + result_output = subprocess.run( + ["python3", str(script_path)], + capture_output=True, + text=True, + timeout=timeout, + cwd=output_dir, + ) + + if result_output.returncode == 0: + # Save the final output + final_output_file = output_dir / "python_decompose_final_output.txt" + with open(final_output_file, "w") as f: + f.write(result_output.stdout) + + print(f"✅ Final output saved to: {final_output_file}") + print("\n" + "=" * 70) + print("Final Output:") + print("=" * 70) + preview = result_output.stdout + print(preview) + return final_output_file + else: + print( + f"❌ Script execution failed with return code {result_output.returncode}" + ) + print(f"Error: {result_output.stderr}") + return None + except subprocess.TimeoutExpired: + print(f"⏱️ Script execution timed out after {timeout} seconds") + return None + except Exception as e: + print(f"❌ Error running script: {e}") + return None + + +def display_results(result: DecompPipelineResult): + """ + Display decomposition results in a formatted way. + + Args: + result: Decomposition results dictionary + """ + print("=" * 70) + print("Decomposition Results") + print("=" * 70) + + print(f"\n📋 Subtasks Identified ({len(result['subtask_list'])}):") + for i, subtask in enumerate(result["subtask_list"], 1): + print(f" {subtask}") + + print(f"\n🔍 Constraints Identified ({len(result['identified_constraints'])}):") + for i, constraint in enumerate(result["identified_constraints"], 1): + print(f" {i}. {constraint['constraint']}") + print(f" Validation: {constraint['val_strategy']}") + + print(f"\n🎯 Detailed Subtasks ({len(result['subtasks'])}):") + for i, subtask_detail in enumerate(result["subtasks"], 1): + print(f"\n Subtask {subtask_detail['subtask']}") + print(f" Tag: {subtask_detail['tag']}") + print(f" Dependencies: {subtask_detail['depends_on'] or 'None'}") + print(f" Input Variables: {subtask_detail['input_vars_required'] or 'None'}") + print(f" Constraints: {len(subtask_detail['constraints'])}") + + +def main(): + # Define a simple task prompt to decompose + task_prompt = textwrap.dedent(""" + Write a short blog post about the benefits of morning exercise. + Include a catchy title, an introduction paragraph, three main benefits + with explanations, and a conclusion that encourages readers to start + their morning exercise routine. + """).strip() + + print("=" * 70) + print("Mellea Decompose Example") + print("=" * 70) + print(f"\nOriginal Task:\n\n{task_prompt.strip()}\n") + + # Step 1: Run decomposition + result = run_decompose(task_prompt) + + # Step 2: Display results + display_results(result) + + # Step 3: Save JSON results + output_dir = Path(__file__).parent + save_decompose_json(result, output_dir) + + # Step 4: Generate Python script + script_path = generate_python_script(result, output_dir) + + # Step 5: Run the generated script (optional) + run_generated_script(script_path, output_dir) + + print("\n" + "=" * 70) + print("✅ Decomposition complete!") + print("=" * 70) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docs/examples/m_decompose/python/python_decompose_result.py b/docs/examples/m_decompose/python/python_decompose_result.py deleted file mode 100644 index 5c52667aa..000000000 --- a/docs/examples/m_decompose/python/python_decompose_result.py +++ /dev/null @@ -1,222 +0,0 @@ -# pytest: skip_always -import textwrap - -import mellea - -# Note: This is an example of an intermediary result from using decompose in python_decompose_example.py, not an example of how to use decompose. - - -m = mellea.start_session() - - -# 1. Create a catchy title for the blog post about the benefits of morning exercise. - - BLOG_TITLE -blog_title = m.instruct( - textwrap.dedent( - R""" - Your task is to create a catchy title for a blog post about the benefits of morning exercise. Follow these steps to accomplish your task: - - 1. **Understand the Topic**: The blog post will focus on the benefits of morning exercise. The title should be engaging and clearly convey the main topic of the post. - - 2. **Identify Key Elements**: Consider the key elements that make morning exercise beneficial. These could include improved mood, increased energy, better focus, and enhanced metabolism. - - 3. **Use Power Words**: Incorporate power words that evoke curiosity, excitement, or a sense of urgency. Examples include "Boost," "Transform," "Unlock," "Energize," and "Revitalize." - - 4. **Keep It Concise**: The title should be short and to the point, ideally between 5 to 10 words. It should be easy to read and remember. - - 5. **Make It Action-Oriented**: Use verbs that encourage action, such as "Start," "Jumpstart," "Kickstart," or "Ignite." - - 6. **Consider SEO**: Think about common search terms related to morning exercise. Including relevant keywords can help improve the post's visibility. - - 7. **Examples for Inspiration**: - - "Jumpstart Your Day: The Power of Morning Exercise" - - "Energize Your Mornings: Unlock the Benefits of Morning Exercise" - - "Transform Your Day with Morning Exercise" - - "Boost Your Energy: The Magic of Morning Workouts" - - "Revitalize Your Mornings: The Benefits of Morning Exercise" - - 8. **Create the Title**: Based on the above guidelines, create a catchy and engaging title for the blog post. Ensure it captures the essence of the topic and entices readers to click and read more. - - Your final answer should be only the title text. - """.strip() - ), - requirements=["Include a catchy title"], -) -assert blog_title.value is not None, 'ERROR: task "blog_title" execution failed' - -# 2. Write an introduction paragraph that sets the stage for the blog post. - - INTRODUCTION -introduction = m.instruct( - textwrap.dedent( - R""" - Your task is to write an engaging introduction paragraph for a blog post about the benefits of morning exercise. The introduction should set the stage for the blog post, capturing the reader's attention and providing a brief overview of what will be discussed. - - To accomplish this, follow these steps: - - 1. **Understand the Context**: - - The blog post is about the benefits of morning exercise. - - The title of the blog post is: {{BLOG_TITLE}} - - 2. **Craft the Introduction**: - - Start with a hook that grabs the reader's attention. This could be a question, a surprising fact, or a relatable scenario. - - Briefly introduce the topic of morning exercise and why it is important. - - Provide a smooth transition to the main benefits that will be discussed in the blog post. - - 3. **Ensure Engagement**: - - Use a conversational and engaging tone to connect with the readers. - - Keep the introduction concise and to the point, ideally between 3 to 5 sentences. - - Here is an example structure to guide your writing: - - **Sentence 1**: Hook to grab the reader's attention. - - **Sentence 2**: Introduce the topic of morning exercise. - - **Sentence 3**: Briefly mention the benefits that will be discussed. - - **Sentence 4**: Transition to the main content of the blog post. - - Ensure that the introduction flows naturally and sets the stage for the rest of the blog post. You should write only the introduction paragraph, do not include the guidance structure. - """.strip() - ), - requirements=["Include an introduction paragraph"], - user_variables={"BLOG_TITLE": blog_title.value}, -) -assert introduction.value is not None, 'ERROR: task "introduction" execution failed' - -# 3. Identify and explain three main benefits of morning exercise with detailed explanations. - - BENEFITS -benefits = m.instruct( - textwrap.dedent( - R""" - Your task is to identify and explain three main benefits of morning exercise with detailed explanations. Follow these steps to accomplish your task: - - First, review the title and introduction created in the previous steps to understand the context and tone of the blog post: - - {{BLOG_TITLE}} - - - {{INTRODUCTION}} - - - Next, research and identify three main benefits of morning exercise. These benefits should be supported by evidence or expert opinions to ensure credibility. - - For each benefit, provide a detailed explanation that includes: - - The specific benefit of morning exercise - - How this benefit positively impacts health, well-being, or daily life - - Any relevant studies, expert opinions, or personal anecdotes that support the benefit - - Ensure that the explanations are clear, concise, and engaging to keep the reader interested. - - Finally, present the three main benefits with their detailed explanations in a structured format that can be easily integrated into the blog post. - """.strip() - ), - requirements=["Include three main benefits with explanations"], - user_variables={"BLOG_TITLE": blog_title.value, "INTRODUCTION": introduction.value}, -) -assert benefits.value is not None, 'ERROR: task "benefits" execution failed' - -# 4. Write a conclusion that encourages readers to start their morning exercise routine. - - CONCLUSION -conclusion = m.instruct( - textwrap.dedent( - R""" - Your task is to write a compelling conclusion for a blog post about the benefits of morning exercise. The conclusion should encourage readers to start their morning exercise routine. Follow these steps to accomplish your task: - - First, review the title and introduction of the blog post to understand the context and tone: - - {{BLOG_TITLE}} - - - {{INTRODUCTION}} - - - Next, consider the three main benefits of morning exercise that have been previously identified and explained: - - {{BENEFITS}} - - - Use the information from the title, introduction, and benefits to craft a conclusion that: - 1. Summarizes the key points discussed in the blog post. - 2. Reinforces the importance of morning exercise. - 3. Encourages readers to take action and start their morning exercise routine. - 4. Maintains a positive and motivating tone. - - Ensure the conclusion is concise, engaging, and leaves readers feeling inspired to make a change in their daily routine. - - Finally, write the conclusion paragraph that encourages readers to start their morning exercise routine. - """.strip() - ), - requirements=[ - "Include a conclusion that encourages readers to start their morning exercise routine" - ], - user_variables={ - "BLOG_TITLE": blog_title.value, - "INTRODUCTION": introduction.value, - "BENEFITS": benefits.value, - }, -) -assert conclusion.value is not None, 'ERROR: task "conclusion" execution failed' - -# 5. Compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post. - - FINAL_BLOG_POST -final_blog_post = m.instruct( - textwrap.dedent( - R""" - Your task is to compile the title, introduction, three main benefits, and conclusion into a single cohesive blog post about the benefits of morning exercise. - - To accomplish this, follow these steps: - - 1. **Review the Components**: - Carefully review the title, introduction, three main benefits, and conclusion that have been generated in the previous steps. These components are provided below: - - - {{BLOG_TITLE}} - - - - {{INTRODUCTION}} - - - - {{BENEFITS}} - - - - {{CONCLUSION}} - - - 2. **Structure the Blog Post**: - Organize the components into a well-structured blog post. The structure should include: - - The catchy title at the beginning. - - The introduction paragraph that sets the stage for the blog post. - - The three main benefits with detailed explanations. - - The conclusion that encourages readers to start their morning exercise routine. - - 3. **Ensure Cohesion**: - Make sure the blog post flows smoothly from one section to the next. The transitions between the introduction, benefits, and conclusion should be natural and logical. - - 4. **Check for Consistency**: - Verify that the tone and style are consistent throughout the blog post. Ensure that the language used in the title, introduction, benefits, and conclusion aligns with the overall theme of the blog post. - - 5. **Final Review**: - Read through the entire blog post to ensure it is cohesive, well-organized, and free of any grammatical or spelling errors. Make any necessary adjustments to improve clarity and readability. - - 6. **Output the Blog Post**: - Provide the final compiled blog post as your answer. Ensure that the output includes only the blog post text without any additional information or instructions. - - By following these steps, you will create a single cohesive blog post that effectively communicates the benefits of morning exercise. - """.strip() - ), - requirements=[ - "Include a catchy title", - "Include an introduction paragraph", - "Include three main benefits with explanations", - "Include a conclusion that encourages readers to start their morning exercise routine", - ], - user_variables={ - "BLOG_TITLE": blog_title.value, - "INTRODUCTION": introduction.value, - "BENEFITS": benefits.value, - "CONCLUSION": conclusion.value, - }, -) -assert final_blog_post.value is not None, ( - 'ERROR: task "final_blog_post" execution failed' -) - - -final_answer = final_blog_post.value - -print(final_answer) \ No newline at end of file From 532ff9a5200528d9ca239a1b7a9228a65bfbb8f3 Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 18:49:45 -0400 Subject: [PATCH 14/26] clean: pre-commit format --- .../subtask_constraint_assign/_subtask_constraint_assign.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py index b5fb1ec0b..264e69c22 100644 --- a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py +++ b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py @@ -131,7 +131,9 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: continue # Remove duplicates while preserving order - subtask_constraint_assign = list(dict.fromkeys(subtask_constraint_assign)) + subtask_constraint_assign = list( + dict.fromkeys(subtask_constraint_assign) + ) result.append( SubtaskPromptConstraintsItem( From 46fd70a7e58126436d837a80a4d34fe9138eb29a Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 19:04:47 -0400 Subject: [PATCH 15/26] fix: pre-commit format --- .../_subtask_constraint_assign.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py index 264e69c22..fa6ac8c13 100644 --- a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py +++ b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py @@ -57,12 +57,12 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: For example ``` [ SubtaskPromptConstraintsItem( - subtask=, - tag=, - prompt_template= - constraints= - ), - ... + subtask=, + tag=, + prompt_template= + constraints= + ), + ... ] ``` @@ -108,6 +108,8 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: subtask_constraint_assign_str_upper = subtask_constraint_assign_str.upper() + subtask_constraint_assign: list[str] = [] + if ( "N/A" in subtask_constraint_assign_str_upper or "N / A" in subtask_constraint_assign_str_upper @@ -116,7 +118,6 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: ): subtask_constraint_assign = [] else: - subtask_constraint_assign = [] for line in subtask_constraint_assign_str.splitlines(): stripped = line.strip() @@ -128,12 +129,9 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: value = stripped[2:].strip() if value: subtask_constraint_assign.append(value) - continue # Remove duplicates while preserving order - subtask_constraint_assign = list( - dict.fromkeys(subtask_constraint_assign) - ) + subtask_constraint_assign = list(dict.fromkeys(subtask_constraint_assign)) result.append( SubtaskPromptConstraintsItem( From 1ef938f35251f9f21927e6f461ba7cca7bfc80eb Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 19:10:05 -0400 Subject: [PATCH 16/26] mod: pre-commit modifcation --- cli/decompose/decompose.py | 2 +- .../_general_instructions.py | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 702cb5f99..72bc5d7ea 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -408,4 +408,4 @@ def run( for decomp_dir in reversed(created_dirs): if decomp_dir.exists() and decomp_dir.is_dir(): shutil.rmtree(decomp_dir) - raise \ No newline at end of file + raise diff --git a/cli/decompose/prompt_modules/general_instructions/_general_instructions.py b/cli/decompose/prompt_modules/general_instructions/_general_instructions.py index 568734ff2..39ec82ade 100644 --- a/cli/decompose/prompt_modules/general_instructions/_general_instructions.py +++ b/cli/decompose/prompt_modules/general_instructions/_general_instructions.py @@ -18,13 +18,11 @@ ) RE_GENERAL_INSTRUCTIONS_OPEN = re.compile( - r"(.*)", - flags=re.IGNORECASE | re.DOTALL, + r"(.*)", flags=re.IGNORECASE | re.DOTALL ) RE_FINAL_SENTENCE = re.compile( - r"\n*All tags are closed and my assignment is finished\.\s*$", - flags=re.IGNORECASE, + r"\n*All tags are closed and my assignment is finished\.\s*$", flags=re.IGNORECASE ) @@ -48,9 +46,7 @@ def _default_parser(generated_str: str) -> str: general_instructions_str = general_instructions_match.group(1).strip() general_instructions_str = re.sub( - RE_FINAL_SENTENCE, - "", - general_instructions_str, + RE_FINAL_SENTENCE, "", general_instructions_str ).strip() return general_instructions_str @@ -77,10 +73,7 @@ def generate( } try: - response = mellea_session.act( - action=action, - model_options=model_options, - ) + response = mellea_session.act(action=action, model_options=model_options) gen_result = response.value except Exception as e: raise BackendGenerationError(f"LLM generation failed: {e}") from e @@ -93,4 +86,4 @@ def generate( return PromptModuleString(gen_result, parser) -general_instructions = _GeneralInstructions() \ No newline at end of file +general_instructions = _GeneralInstructions() From fc89d786e9bfb366a490a5db95eb69626b8c3c0e Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 20:55:47 -0400 Subject: [PATCH 17/26] mod: pre-commit clean --- .../subtask_constraint_assign/_subtask_constraint_assign.py | 4 +++- docs/examples/m_decompose/python/python_decompose_example.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py index fa6ac8c13..02ab63f5b 100644 --- a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py +++ b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py @@ -131,7 +131,9 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: subtask_constraint_assign.append(value) # Remove duplicates while preserving order - subtask_constraint_assign = list(dict.fromkeys(subtask_constraint_assign)) + subtask_constraint_assign = list( + dict.fromkeys(subtask_constraint_assign) + ) result.append( SubtaskPromptConstraintsItem( diff --git a/docs/examples/m_decompose/python/python_decompose_example.py b/docs/examples/m_decompose/python/python_decompose_example.py index 701b414ea..757296b56 100644 --- a/docs/examples/m_decompose/python/python_decompose_example.py +++ b/docs/examples/m_decompose/python/python_decompose_example.py @@ -225,4 +225,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() From ef56b2cd5a40244c190f5daa7c03b71d22cad30e Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 21:32:36 -0400 Subject: [PATCH 18/26] mod: pre-commit clean --- .../python/python_decompose_example.py | 228 ------------------ 1 file changed, 228 deletions(-) delete mode 100644 docs/examples/m_decompose/python/python_decompose_example.py diff --git a/docs/examples/m_decompose/python/python_decompose_example.py b/docs/examples/m_decompose/python/python_decompose_example.py deleted file mode 100644 index 757296b56..000000000 --- a/docs/examples/m_decompose/python/python_decompose_example.py +++ /dev/null @@ -1,228 +0,0 @@ -# pytest: ollama, e2e, slow, qualitative -#!/usr/bin/env python3 -""" -Example: Using Mellea's decompose functionality programmatically - -This script demonstrates how to use the decompose pipeline from Python code -to break down a complex task into subtasks with generated prompts. -""" - -import json -import subprocess -import textwrap -from pathlib import Path - -from jinja2 import Environment, FileSystemLoader - -# Import the decompose pipeline from the CLI module -from cli.decompose.pipeline import DecompBackend, DecompPipelineResult, decompose - - -def run_decompose(task_prompt: str) -> DecompPipelineResult: - """ - Run the decompose pipeline on a task prompt. - - Args: - task_prompt: The task description to decompose - - Returns: - Dictionary containing decomposition results - """ - print("Running decomposition pipeline...\n") - - result = decompose( - task_prompt=task_prompt, - model_id="granite3.3:8b", # Note micro will not properly create tags, need 8b - backend=DecompBackend.ollama, # Use Ollama backend - backend_req_timeout=300, # 5 minute timeout - ) - - return result - - -def save_decompose_json( - result: DecompPipelineResult, - output_dir: Path, - filename: str = "python_decompose_result.json", -) -> Path: - """ - Save decomposition results to a JSON file. - - Args: - result: Decomposition results dictionary - output_dir: Directory to save the file - filename: Name of the output file - - Returns: - Path to the saved JSON file - """ - json_output_file = output_dir / filename - - with open(json_output_file, "w") as f: - json.dump(result, f, indent=2) - - print(f"💾 JSON results saved to: {json_output_file}") - return json_output_file - - -def generate_python_script( - result: DecompPipelineResult, - output_dir: Path, - filename: str = "python_decompose_result.py", -) -> Path: - """ - Generate an executable Python script from decomposition results. - - Args: - result: Decomposition results dictionary - output_dir: Directory to save the file - filename: Name of the output Python file - - Returns: - Path to the generated Python script - """ - print("\n📝 Generating executable Python script...") - - # Load the template from the CLI decompose directory - cli_decompose_dir = ( - Path(__file__).parent.parent.parent.parent.parent / "cli" / "decompose" - ) - environment = Environment( - loader=FileSystemLoader(cli_decompose_dir), autoescape=False - ) - m_template = environment.get_template("m_decomp_result_v1.py.jinja2") - - # Render the template with the decomposition results - python_script_content = m_template.render( - subtasks=result["subtasks"], - user_inputs=[], # No user inputs for this simple example - ) - - # Save the generated Python script - py_output_file = output_dir / filename - with open(py_output_file, "w") as f: - f.write(python_script_content + "\n") - - print(f"💾 Generated Python script saved to: {py_output_file}") - return py_output_file - - -def run_generated_script( - script_path: Path, output_dir: Path, timeout: int = 600 -) -> Path | None: - """ - Execute the generated Python script to produce final output. - - Args: - script_path: Path to the Python script to execute - output_dir: Directory to save the final output - timeout: Maximum execution time in seconds - - Returns: - Path to the final output file if successful, None otherwise - """ - print("\n🚀 Running the generated script to produce final output...") - print(" (This may take a few minutes as it calls the LLM for each subtask)") - - try: - result_output = subprocess.run( - ["python3", str(script_path)], - capture_output=True, - text=True, - timeout=timeout, - cwd=output_dir, - ) - - if result_output.returncode == 0: - # Save the final output - final_output_file = output_dir / "python_decompose_final_output.txt" - with open(final_output_file, "w") as f: - f.write(result_output.stdout) - - print(f"✅ Final output saved to: {final_output_file}") - print("\n" + "=" * 70) - print("Final Output:") - print("=" * 70) - preview = result_output.stdout - print(preview) - return final_output_file - else: - print( - f"❌ Script execution failed with return code {result_output.returncode}" - ) - print(f"Error: {result_output.stderr}") - return None - except subprocess.TimeoutExpired: - print(f"⏱️ Script execution timed out after {timeout} seconds") - return None - except Exception as e: - print(f"❌ Error running script: {e}") - return None - - -def display_results(result: DecompPipelineResult): - """ - Display decomposition results in a formatted way. - - Args: - result: Decomposition results dictionary - """ - print("=" * 70) - print("Decomposition Results") - print("=" * 70) - - print(f"\n📋 Subtasks Identified ({len(result['subtask_list'])}):") - for i, subtask in enumerate(result["subtask_list"], 1): - print(f" {subtask}") - - print(f"\n🔍 Constraints Identified ({len(result['identified_constraints'])}):") - for i, constraint in enumerate(result["identified_constraints"], 1): - print(f" {i}. {constraint['constraint']}") - print(f" Validation: {constraint['val_strategy']}") - - print(f"\n🎯 Detailed Subtasks ({len(result['subtasks'])}):") - for i, subtask_detail in enumerate(result["subtasks"], 1): - print(f"\n Subtask {subtask_detail['subtask']}") - print(f" Tag: {subtask_detail['tag']}") - print(f" Dependencies: {subtask_detail['depends_on'] or 'None'}") - print(f" Input Variables: {subtask_detail['input_vars_required'] or 'None'}") - print(f" Constraints: {len(subtask_detail['constraints'])}") - - -def main(): - # Define a simple task prompt to decompose - task_prompt = textwrap.dedent(""" - Write a short blog post about the benefits of morning exercise. - Include a catchy title, an introduction paragraph, three main benefits - with explanations, and a conclusion that encourages readers to start - their morning exercise routine. - """).strip() - - print("=" * 70) - print("Mellea Decompose Example") - print("=" * 70) - print(f"\nOriginal Task:\n\n{task_prompt.strip()}\n") - - # Step 1: Run decomposition - result = run_decompose(task_prompt) - - # Step 2: Display results - display_results(result) - - # Step 3: Save JSON results - output_dir = Path(__file__).parent - save_decompose_json(result, output_dir) - - # Step 4: Generate Python script - script_path = generate_python_script(result, output_dir) - - # Step 5: Run the generated script (optional) - run_generated_script(script_path, output_dir) - - print("\n" + "=" * 70) - print("✅ Decomposition complete!") - print("=" * 70) - - -if __name__ == "__main__": - main() From ab7082f09b95131842d2ea131f91d28fe2dc2193 Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 1 Apr 2026 21:35:27 -0400 Subject: [PATCH 19/26] mod: pre-commit clean --- .../python/python_decompose_example.py | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 docs/examples/m_decompose/python/python_decompose_example.py diff --git a/docs/examples/m_decompose/python/python_decompose_example.py b/docs/examples/m_decompose/python/python_decompose_example.py new file mode 100644 index 000000000..757296b56 --- /dev/null +++ b/docs/examples/m_decompose/python/python_decompose_example.py @@ -0,0 +1,228 @@ +# pytest: ollama, e2e, slow, qualitative +#!/usr/bin/env python3 +""" +Example: Using Mellea's decompose functionality programmatically + +This script demonstrates how to use the decompose pipeline from Python code +to break down a complex task into subtasks with generated prompts. +""" + +import json +import subprocess +import textwrap +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + +# Import the decompose pipeline from the CLI module +from cli.decompose.pipeline import DecompBackend, DecompPipelineResult, decompose + + +def run_decompose(task_prompt: str) -> DecompPipelineResult: + """ + Run the decompose pipeline on a task prompt. + + Args: + task_prompt: The task description to decompose + + Returns: + Dictionary containing decomposition results + """ + print("Running decomposition pipeline...\n") + + result = decompose( + task_prompt=task_prompt, + model_id="granite3.3:8b", # Note micro will not properly create tags, need 8b + backend=DecompBackend.ollama, # Use Ollama backend + backend_req_timeout=300, # 5 minute timeout + ) + + return result + + +def save_decompose_json( + result: DecompPipelineResult, + output_dir: Path, + filename: str = "python_decompose_result.json", +) -> Path: + """ + Save decomposition results to a JSON file. + + Args: + result: Decomposition results dictionary + output_dir: Directory to save the file + filename: Name of the output file + + Returns: + Path to the saved JSON file + """ + json_output_file = output_dir / filename + + with open(json_output_file, "w") as f: + json.dump(result, f, indent=2) + + print(f"💾 JSON results saved to: {json_output_file}") + return json_output_file + + +def generate_python_script( + result: DecompPipelineResult, + output_dir: Path, + filename: str = "python_decompose_result.py", +) -> Path: + """ + Generate an executable Python script from decomposition results. + + Args: + result: Decomposition results dictionary + output_dir: Directory to save the file + filename: Name of the output Python file + + Returns: + Path to the generated Python script + """ + print("\n📝 Generating executable Python script...") + + # Load the template from the CLI decompose directory + cli_decompose_dir = ( + Path(__file__).parent.parent.parent.parent.parent / "cli" / "decompose" + ) + environment = Environment( + loader=FileSystemLoader(cli_decompose_dir), autoescape=False + ) + m_template = environment.get_template("m_decomp_result_v1.py.jinja2") + + # Render the template with the decomposition results + python_script_content = m_template.render( + subtasks=result["subtasks"], + user_inputs=[], # No user inputs for this simple example + ) + + # Save the generated Python script + py_output_file = output_dir / filename + with open(py_output_file, "w") as f: + f.write(python_script_content + "\n") + + print(f"💾 Generated Python script saved to: {py_output_file}") + return py_output_file + + +def run_generated_script( + script_path: Path, output_dir: Path, timeout: int = 600 +) -> Path | None: + """ + Execute the generated Python script to produce final output. + + Args: + script_path: Path to the Python script to execute + output_dir: Directory to save the final output + timeout: Maximum execution time in seconds + + Returns: + Path to the final output file if successful, None otherwise + """ + print("\n🚀 Running the generated script to produce final output...") + print(" (This may take a few minutes as it calls the LLM for each subtask)") + + try: + result_output = subprocess.run( + ["python3", str(script_path)], + capture_output=True, + text=True, + timeout=timeout, + cwd=output_dir, + ) + + if result_output.returncode == 0: + # Save the final output + final_output_file = output_dir / "python_decompose_final_output.txt" + with open(final_output_file, "w") as f: + f.write(result_output.stdout) + + print(f"✅ Final output saved to: {final_output_file}") + print("\n" + "=" * 70) + print("Final Output:") + print("=" * 70) + preview = result_output.stdout + print(preview) + return final_output_file + else: + print( + f"❌ Script execution failed with return code {result_output.returncode}" + ) + print(f"Error: {result_output.stderr}") + return None + except subprocess.TimeoutExpired: + print(f"⏱️ Script execution timed out after {timeout} seconds") + return None + except Exception as e: + print(f"❌ Error running script: {e}") + return None + + +def display_results(result: DecompPipelineResult): + """ + Display decomposition results in a formatted way. + + Args: + result: Decomposition results dictionary + """ + print("=" * 70) + print("Decomposition Results") + print("=" * 70) + + print(f"\n📋 Subtasks Identified ({len(result['subtask_list'])}):") + for i, subtask in enumerate(result["subtask_list"], 1): + print(f" {subtask}") + + print(f"\n🔍 Constraints Identified ({len(result['identified_constraints'])}):") + for i, constraint in enumerate(result["identified_constraints"], 1): + print(f" {i}. {constraint['constraint']}") + print(f" Validation: {constraint['val_strategy']}") + + print(f"\n🎯 Detailed Subtasks ({len(result['subtasks'])}):") + for i, subtask_detail in enumerate(result["subtasks"], 1): + print(f"\n Subtask {subtask_detail['subtask']}") + print(f" Tag: {subtask_detail['tag']}") + print(f" Dependencies: {subtask_detail['depends_on'] or 'None'}") + print(f" Input Variables: {subtask_detail['input_vars_required'] or 'None'}") + print(f" Constraints: {len(subtask_detail['constraints'])}") + + +def main(): + # Define a simple task prompt to decompose + task_prompt = textwrap.dedent(""" + Write a short blog post about the benefits of morning exercise. + Include a catchy title, an introduction paragraph, three main benefits + with explanations, and a conclusion that encourages readers to start + their morning exercise routine. + """).strip() + + print("=" * 70) + print("Mellea Decompose Example") + print("=" * 70) + print(f"\nOriginal Task:\n\n{task_prompt.strip()}\n") + + # Step 1: Run decomposition + result = run_decompose(task_prompt) + + # Step 2: Display results + display_results(result) + + # Step 3: Save JSON results + output_dir = Path(__file__).parent + save_decompose_json(result, output_dir) + + # Step 4: Generate Python script + script_path = generate_python_script(result, output_dir) + + # Step 5: Run the generated script (optional) + run_generated_script(script_path, output_dir) + + print("\n" + "=" * 70) + print("✅ Decomposition complete!") + print("=" * 70) + + +if __name__ == "__main__": + main() From 1db129e260739b0cc3f47b5c72d28f7d45194da6 Mon Sep 17 00:00:00 2001 From: Bobby Date: Fri, 3 Apr 2026 22:33:41 -0400 Subject: [PATCH 20/26] add: enable the v3 --- cli/decompose/decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 72bc5d7ea..e441169ef 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -42,7 +42,7 @@ class DecompVersion(StrEnum): latest = "latest" v1 = "v1" v2 = "v2" - # v3 = "v3" + v3 = "v3" this_file_dir = Path(__file__).resolve().parent From 718115d997ee1eb51635676cf653edd18281e26a Mon Sep 17 00:00:00 2001 From: Bobby Date: Mon, 6 Apr 2026 10:30:31 -0400 Subject: [PATCH 21/26] clean: version verify --- test/decompose/test_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/decompose/test_decompose.py b/test/decompose/test_decompose.py index d23cbcf62..c714c6975 100644 --- a/test/decompose/test_decompose.py +++ b/test/decompose/test_decompose.py @@ -219,7 +219,7 @@ def get_template(self, template_name: str) -> DummyTemplate: version=DecompVersion.latest, ) - assert requested_templates == ["m_decomp_result_v2.py.jinja2"] + assert requested_templates == ["m_decomp_result_v3.py.jinja2"] def test_successful_run_writes_outputs( self, From ff8bed6dab4bec24cf6aaa6cc2279726078a2cfd Mon Sep 17 00:00:00 2001 From: Bobby Date: Thu, 9 Apr 2026 11:10:44 -0400 Subject: [PATCH 22/26] upd: unify the constraint tag parsing with more item variants --- .../_subtask_constraint_assign.py | 264 ++++++++++++------ 1 file changed, 177 insertions(+), 87 deletions(-) diff --git a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py index 02ab63f5b..301e30360 100644 --- a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py +++ b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py @@ -6,6 +6,9 @@ from mellea.backends import ModelOption from mellea.stdlib.components import Message +from mellea.core import FancyLogger +FancyLogger.get_logger().setLevel("DEBUG") + from .._prompt_modules import PromptModule, PromptModuleString from ._exceptions import BackendGenerationError, TagExtractionError from ._prompt import get_system_prompt, get_user_prompt @@ -23,6 +26,19 @@ flags=re.IGNORECASE | re.DOTALL, ) +# Regex to match common list formats produced by LLMs: +# - bullet lists: -, *, •, –, — +# - numbered lists: 1. 1) +RE_LIST_ITEM = re.compile( + r"^\s*(?:[-*•]|[–—]|(?:\d+)[.)])\s*(.+?)\s*$" +) + +# Regex to remove trailing "closing sentence" often generated by LLMs +RE_TRAILING_CLOSURE = re.compile( + r"\n*All tags are closed and my assignment is finished\.\s*$", + flags=re.IGNORECASE, +) + class SubtaskConstraintAssignArgs(TypedDict): subtasks_tags_and_prompts: Sequence[tuple[str, str, str]] @@ -31,6 +47,53 @@ class SubtaskConstraintAssignArgs(TypedDict): @final class _SubtaskConstraintAssign(PromptModule): + + @staticmethod + def _is_na_value(text: str) -> bool: + """ + Check if the entire content represents a "no constraints" signal. + + This avoids false positives such as: + "- If missing, return N/A" + + Only returns True if the WHOLE text is equivalent to N/A. + """ + normalized = re.sub(r"\s+", "", text).upper() + return normalized in {"N/A", "NA"} + + @staticmethod + def _extract_constraints(text: str) -> list[str]: + """ + Extract constraints from list-style text. + + Supports: + - "- item" + - "* item" + - "• item" + - "1. item" + - "1) item" + - "– item" + - "— item" + + Returns: + Deduplicated list while preserving order. + """ + constraints: list[str] = [] + + for line in text.splitlines(): + stripped = line.strip() + if not stripped: + continue + + match = RE_LIST_ITEM.match(line) + if match: + value = match.group(1).strip() + if value: + constraints.append(value) + + # Remove duplicates while preserving order + return list(dict.fromkeys(constraints)) + @staticmethod def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: r"""Default parser of the `subtask_constraint_assign` module. @@ -39,49 +102,66 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: on the size and capabilities of the LLM used. The results are also not guaranteed, so take a look at this module's Exceptions and plan for unreliable results._ + This parser processes the serialized output produced by the + `subtask_constraint_assign.generate()` method and converts it into structured + constraint assignments for each subtask. + + The parser is designed to be robust against minor formatting variations in LLM + responses. In particular, it: + - extracts content from `` tags when present; + - falls back to the raw generated text when the expected tags are missing; + - supports multiple list formats commonly produced by LLMs, such as `-`, `*`, + `•`, `1.`, and `1)`; + - treats the entire content as "no constraints" only when the full content is + equivalent to `N/A` or `NA`; + - falls back to using the full text as a single constraint when non-empty content + is present but no list-style items can be parsed. + Args: - generated_str (`str`): The LLM's answer to be parsed - (this `str` contains the result of the LLM calls - for each subtask, separated by a character combination - to enable parsing). + generated_str (`str`): The LLM output to be parsed. + This string contains the generation results for each subtask, + separated by internal delimiters to enable structured parsing. Returns: list[SubtaskPromptConstraintsItem]: A `list` of - `NamedTuple` (`SubtaskPromptConstraintsItem`) where each - `tuple` contains the "subtask" (`str`), its "tag" (`str`), its - generated "prompt_template" (`str`), and - its assigned "constraints" (`list[str]`). + `SubtaskPromptConstraintsItem` objects where each item contains: + - `subtask` (`str`): the subtask title or description; + - `tag` (`str`): the variable/tag associated with the subtask; + - `prompt_template` (`str`): the generated prompt template for the subtask; + - `constraints` (`list[str]`): the constraints assigned to that subtask. - Note that the result "constraints" list can be empty. + Note that the `constraints` list can be empty. - For example + For example: ``` - [ SubtaskPromptConstraintsItem( - subtask=, - tag=, - prompt_template= - constraints= - ), - ... + [ + SubtaskPromptConstraintsItem( + subtask=, + tag=, + prompt_template=, + constraints=, + ), + ... ] ``` - You can use dot notation to access the values. For example + You can use dot notation to access the values. For example: ``` - result: PromptModuleString = # Result of the subtask_constraint_assign.generate() method + result: PromptModuleString = ... # Result of subtask_constraint_assign.generate() parsed_result: list[SubtaskPromptConstraintsItem] = result.parse() - subtask_0: str = result[0].subtask - tag_0: str = result[0].tag - prompt_template_0: str = result[0].prompt_template - constraints_0: list[str] = result[0].constraints + subtask_0: str = parsed_result[0].subtask + tag_0: str = parsed_result[0].tag + prompt_template_0: str = parsed_result[0].prompt_template + constraints_0: list[str] = parsed_result[0].constraints ``` Raises: - TagExtractionError: An error occurred trying to extract content from the - generated output. The LLM probably failed to open and close - the \ tags for one of the subtasks. + This parser does not intentionally raise `TagExtractionError` when the + expected tags are missing. Instead, it attempts to recover by falling back + to raw generated text. Parsing may still fail indirectly if the serialized + generation format is malformed beyond recovery. """ gen_data = re.findall(RE_GEN_DATA_FORMAT, generated_str) @@ -90,51 +170,47 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: for data in gen_data: data = cast(tuple[str, str, str, str], data) + # Try extracting content inside tags subtask_constraint_assign_match = re.search(RE_ASSIGNED_CONS, data[3]) - # ===== fallback: use raw text when there is no tag ===== - subtask_constraint_assign_str: str = ( - subtask_constraint_assign_match.group(1).strip() - if subtask_constraint_assign_match - else data[3].strip() - ) + if subtask_constraint_assign_match: + subtask_constraint_assign_str = ( + subtask_constraint_assign_match.group(1).strip() + ) + else: + # Fallback to raw text if tags are missing + FancyLogger.get_logger().warning( + "Expected tags missing from LLM response; falling back to raw response text. " + "Downstream stages may receive unstructured content." + ) + subtask_constraint_assign_str = data[3].strip() + # Remove common trailing LLM artifacts subtask_constraint_assign_str = re.sub( - r"\n*All tags are closed and my assignment is finished\.\s*$", + RE_TRAILING_CLOSURE, "", subtask_constraint_assign_str, - flags=re.IGNORECASE, ).strip() - subtask_constraint_assign_str_upper = subtask_constraint_assign_str.upper() + # Handle "no constraints" case + if _SubtaskConstraintAssign._is_na_value(subtask_constraint_assign_str): + subtask_constraint_assign: list[str] = [] - subtask_constraint_assign: list[str] = [] - - if ( - "N/A" in subtask_constraint_assign_str_upper - or "N / A" in subtask_constraint_assign_str_upper - or "N/ A" in subtask_constraint_assign_str_upper - or "N /A" in subtask_constraint_assign_str_upper - ): - subtask_constraint_assign = [] else: - for line in subtask_constraint_assign_str.splitlines(): - stripped = line.strip() - - if not stripped: - continue - - # Only keep lines starting with "- " - if stripped.startswith("- "): - value = stripped[2:].strip() - if value: - subtask_constraint_assign.append(value) - - # Remove duplicates while preserving order - subtask_constraint_assign = list( - dict.fromkeys(subtask_constraint_assign) + # Try extracting list-style constraints + subtask_constraint_assign = _SubtaskConstraintAssign._extract_constraints( + subtask_constraint_assign_str ) + # -------- Secondary fallback -------- + # If content exists but no list items were parsed, + # treat the whole text as a single constraint. + if subtask_constraint_assign_str and not subtask_constraint_assign: + FancyLogger.get_logger().warning( + "No list-style constraints detected; falling back to full text as a single constraint." + ) + subtask_constraint_assign = [subtask_constraint_assign_str] + result.append( SubtaskPromptConstraintsItem( subtask=data[0].strip(), @@ -161,55 +237,69 @@ def generate( # type: ignore[override] # About the mypy ignore statement above: https://github.com/python/mypy/issues/3737 **kwargs: Unpack[SubtaskConstraintAssignArgs], ) -> PromptModuleString[T]: - """Receives a list of subtasks (with their tags and template prompts) and a list of - constraints written in natural language. + """Receives a list of subtasks (with their tags and template prompts) and a list + of constraints written in natural language. - Selects and assign, to each subtask, the constraints that the LLM judges - to be appropriate (amongst the provided constraint list) to each subtask. + Selects and assigns, to each subtask, the constraints that the LLM judges + to be appropriate (among the provided constraint list) for that subtask. _**Disclaimer**: This is a LLM-prompting module, so the results will vary depending on the size and capabilities of the LLM used. The results are also not guaranteed, so take a look at this module's Exceptions and plan for unreliable results._ Args: - mellea_session (`MelleaSession`): A mellea session with a backend. - input_str (`None`, optional): This module doesn't use the "input_str" argument. - max_new_tokens (`int`, optional): Maximum tokens to generate. - Try increasing the value if you are getting `TagExtractionError`. - Defaults to `8192`. - parser (`Callable[[str], Any]`, optional): A string parsing function. + mellea_session (`MelleaSession`): A mellea session configured with a backend. + input_str (`None`, optional): This module does not use the `input_str` + argument. It is kept for interface compatibility. + max_new_tokens (`int`, optional): Maximum number of tokens to generate + for each LLM call. Try increasing this value if the model is truncating + its answers. Defaults to `4096`. + parser (`Callable[[str], Any]`, optional): A parsing function used by the + returned `PromptModuleString.parse()` method. Defaults to `_SubtaskConstraintAssign._default_parser`. - subtasks_tags_and_prompts (`Sequence[tuple[str, str, str]]`): A list of subtasks, - their respective tags and prompts. + subtasks_tags_and_prompts (`Sequence[tuple[str, str, str]]`): A sequence of + subtasks and their respective tags and prompt templates. + + This was designed to receive the parsed result of the + `subtask_prompt_generator` module, but this is not required, as long as + the provided data follows the expected format. - This was designed to receive the parsed result of the `subtask_prompt_generator` - module, but it's not required, you are able to provide arguments in the correct format. + Each item must be a `tuple[str, str, str]` where: + - the first element is the subtask title or description in natural language; + - the second element is a descriptive tag/variable name for that subtask; + - the third element is the prompt template to be executed by an LLM. - The list must be composed of `tuple[str, str, str]` objects where the first position is - the subtask title/description in natural language, the second position is a tag/variable - with a descriptive name related to its subtask, and the third position is the template - prompt for an LLM to execute the subtask. e.g. + Example: ``` subtasks_tags_and_prompts = [ - ("1. Read the document and write a summary", "DOCUMENT_SUMMARY", ""), - ("2. Write the 3 most important phrases as bullets", "IMPORTANT_PHRASES", "") + ( + "1. Read the document and write a summary", + "DOCUMENT_SUMMARY", + "", + ), + ( + "2. Write the 3 most important phrases as bullets", + "IMPORTANT_PHRASES", + "", + ), ] ``` constraint_list (`Sequence[str]`): A list of constraints written in natural language. - This was designed to take in a list of constraints identified from the prompt - that originated the subtasks provided, so they can be correctly - distributed and assigned to the subtasks. + This was designed to receive constraints extracted from the original user + request so they can be distributed across the generated subtasks. Returns: - PromptModuleString: A `PromptModuleString` class containing the generated output. + PromptModuleString: A `PromptModuleString` containing the serialized LLM output. - The `PromptModuleString` class behaves like a `str`, but with an additional `parse()` method - to execute the parsing function passed in the `parser` argument of - this method (the `parser` argument defaults to `_SubtaskConstraintAssign._default_parser`). + The `PromptModuleString` behaves like a `str`, but also provides a `parse()` + method that applies the parsing function passed through the `parser` + argument. By default, this is + `_SubtaskConstraintAssign._default_parser`. Raises: - BackendGenerationError: Some error occurred during the LLM generation call. + BackendGenerationError: Raised when an LLM generation call fails, or when the + backend returns `None` as the generated value. """ system_prompt = get_system_prompt() From 08fe6882add42c674e3607d7e839e5ef4734475d Mon Sep 17 00:00:00 2001 From: Bobby Date: Thu, 9 Apr 2026 12:11:01 -0400 Subject: [PATCH 23/26] upd: update decompose tests up-to-date --- cli/decompose/decompose.py | 17 +++++++ test/decompose/test_decompose.py | 81 +++++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index e441169ef..9d2572a28 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -226,6 +226,15 @@ def run( case_sensitive=False, ), ] = LogMode.demo, + enable_script_run: Annotated[ + bool, + typer.Option( + help=( + "When true, generated scripts expose argparse runtime options " + "for backend, model, endpoint, and API key overrides." + ) + ), + ] = False, ) -> None: """Runs the ``m decompose`` CLI workflow and writes generated outputs. @@ -253,6 +262,8 @@ def run( prompts and programs. Each name must be a valid non-keyword Python identifier. log_mode: Logging verbosity for CLI and pipeline execution. + enable_script_run: Whether generated scripts should expose argparse + runtime options. Defaults to ``False``. Raises: AssertionError: If ``out_name`` is invalid, ``out_dir`` does not name an @@ -277,6 +288,7 @@ def run( logger.info("model_id : %s", model_id) logger.info("version : %s", version.value) logger.info("log_mode : %s", log_mode.value) + logger.info("script options : %s", enable_script_run) logger.info("input_vars : %s", input_var or "[]") environment = Environment( @@ -393,6 +405,11 @@ def run( subtasks=decomp_data["subtasks"], user_inputs=input_var, identified_constraints=decomp_data["identified_constraints"], + model_id=model_id, + backend=backend.value, + backend_endpoint=backend_endpoint, + backend_api_key=backend_api_key, + enable_script_run=enable_script_run, ) + "\n" ) diff --git a/test/decompose/test_decompose.py b/test/decompose/test_decompose.py index c714c6975..d44b090f6 100644 --- a/test/decompose/test_decompose.py +++ b/test/decompose/test_decompose.py @@ -10,7 +10,7 @@ import pytest -from cli.decompose.decompose import DecompVersion, run +from cli.decompose.decompose import DecompVersion, reorder_subtasks, run from cli.decompose.logging import LogMode from cli.decompose.pipeline import DecompBackend, DecompPipelineResult @@ -32,18 +32,39 @@ def get_template(self, template_name: str) -> DummyTemplate: return DummyTemplate() -def make_decomp_result() -> DecompPipelineResult: - """Create a minimal valid decomposition result for CLI tests.""" +def make_decomp_result(with_code_validation: bool = False) -> DecompPipelineResult: + """Create a valid decomposition result for CLI tests.""" + identified_constraints = [] + subtask_constraints = [] + + if with_code_validation: + identified_constraints = [ + { + "constraint": "Return valid JSON.", + "val_strategy": "code", + "val_fn": "def validate_input(text: str) -> bool:\n return text.startswith('{')", + "val_fn_name": "val_fn_1", + } + ] + subtask_constraints = [ + { + "constraint": "Return valid JSON.", + "val_strategy": "code", + "val_fn": "def validate_input(text: str) -> bool:\n return text.startswith('{')", + "val_fn_name": "val_fn_1", + } + ] + return { "original_task_prompt": "Test task prompt", "subtask_list": ["Task A"], - "identified_constraints": [], + "identified_constraints": identified_constraints, "subtasks": [ { - "subtask": "Task A", + "subtask": "1. Task A", "tag": "TASK_A", - "general_instructions": "", - "constraints": [], + "general_instructions": "Keep the answer concise.", + "constraints": subtask_constraints, "prompt_template": "Do A", "input_vars_required": [], "depends_on": [], @@ -192,18 +213,12 @@ def test_latest_version_resolves_to_last_declared_version( """``latest`` resolves to the last declared enum version.""" input_file = write_input_file(tmp_path, "Test") requested_templates: list[str] = [] + environment = Mock() + environment.get_template.side_effect = lambda template_name: ( + requested_templates.append(template_name) or DummyTemplate() + ) - class TrackingEnvironment: - """Tracking Jinja environment for template resolution assertions.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - pass - - def get_template(self, template_name: str) -> DummyTemplate: - requested_templates.append(template_name) - return DummyTemplate() - - monkeypatch.setattr("cli.decompose.decompose.Environment", TrackingEnvironment) + monkeypatch.setattr("cli.decompose.decompose.Environment", lambda *args, **kwargs: environment) monkeypatch.setattr( "cli.decompose.decompose.FileSystemLoader", lambda *args, **kwargs: None ) @@ -247,6 +262,36 @@ def test_successful_run_writes_outputs( assert (out_path / "validations").exists() assert (out_path / "validations" / "__init__.py").exists() + def test_latest_template_writes_validation_modules_for_code_constraints( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + patch_validate_filename: None, + patch_logging: Mock, + ) -> None: + """Latest template includes code-validation imports and writes modules.""" + input_file = write_input_file(tmp_path, "Generate JSON.") + + monkeypatch.setattr( + "cli.decompose.decompose.pipeline.decompose", + lambda **kwargs: make_decomp_result(with_code_validation=True), + ) + + run(out_dir=tmp_path, out_name="validated_case", input_file=input_file) + + validation_path = tmp_path / "validated_case" / "validations" / "val_fn_1.py" + program_path = tmp_path / "validated_case" / "validated_case.py" + + assert validation_path.exists() + assert "def validate_input" in validation_path.read_text(encoding="utf-8") + + program = program_path.read_text(encoding="utf-8") + assert "from mellea.stdlib.requirements import req" in program + assert ( + "from validations.val_fn_1 import validate_input as val_fn_1" in program + ) + assert "Keep the answer concise." in program + def test_multi_line_input_file_creates_numbered_jobs( self, tmp_path: Path, From 616d91d4d684420ac12c8e66d14cfa5ab9d23023 Mon Sep 17 00:00:00 2001 From: Bobby Date: Thu, 9 Apr 2026 12:41:37 -0400 Subject: [PATCH 24/26] clean: pre-commit --- test/decompose/test_decompose.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/decompose/test_decompose.py b/test/decompose/test_decompose.py index d44b090f6..b3297d5d0 100644 --- a/test/decompose/test_decompose.py +++ b/test/decompose/test_decompose.py @@ -12,7 +12,7 @@ from cli.decompose.decompose import DecompVersion, reorder_subtasks, run from cli.decompose.logging import LogMode -from cli.decompose.pipeline import DecompBackend, DecompPipelineResult +from cli.decompose.pipeline import ConstraintResult, DecompBackend, DecompPipelineResult class DummyTemplate: @@ -34,8 +34,8 @@ def get_template(self, template_name: str) -> DummyTemplate: def make_decomp_result(with_code_validation: bool = False) -> DecompPipelineResult: """Create a valid decomposition result for CLI tests.""" - identified_constraints = [] - subtask_constraints = [] + identified_constraints: list[ConstraintResult] = [] + subtask_constraints: list[ConstraintResult] = [] if with_code_validation: identified_constraints = [ @@ -214,11 +214,16 @@ def test_latest_version_resolves_to_last_declared_version( input_file = write_input_file(tmp_path, "Test") requested_templates: list[str] = [] environment = Mock() - environment.get_template.side_effect = lambda template_name: ( - requested_templates.append(template_name) or DummyTemplate() - ) - monkeypatch.setattr("cli.decompose.decompose.Environment", lambda *args, **kwargs: environment) + def get_template(template_name: str) -> DummyTemplate: + requested_templates.append(template_name) + return DummyTemplate() + + environment.get_template.side_effect = get_template + + monkeypatch.setattr( + "cli.decompose.decompose.Environment", lambda *args, **kwargs: environment + ) monkeypatch.setattr( "cli.decompose.decompose.FileSystemLoader", lambda *args, **kwargs: None ) From 28046c4ea51bd14382b8853523f5cd0a8ea64ea4 Mon Sep 17 00:00:00 2001 From: Bobby Date: Thu, 9 Apr 2026 12:50:08 -0400 Subject: [PATCH 25/26] clean: pre-commit module --- .../_subtask_constraint_assign.py | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py index 301e30360..50a152648 100644 --- a/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py +++ b/cli/decompose/prompt_modules/subtask_constraint_assign/_subtask_constraint_assign.py @@ -4,16 +4,16 @@ from mellea import MelleaSession from mellea.backends import ModelOption -from mellea.stdlib.components import Message - from mellea.core import FancyLogger -FancyLogger.get_logger().setLevel("DEBUG") +from mellea.stdlib.components import Message from .._prompt_modules import PromptModule, PromptModuleString from ._exceptions import BackendGenerationError, TagExtractionError from ._prompt import get_system_prompt, get_user_prompt from ._types import SubtaskPromptConstraintsItem +FancyLogger.get_logger().setLevel("DEBUG") + T = TypeVar("T") RE_GEN_DATA_FORMAT = re.compile( @@ -27,16 +27,13 @@ ) # Regex to match common list formats produced by LLMs: -# - bullet lists: -, *, •, –, — +# - bullet lists: -, *, •, en dash, em dash # - numbered lists: 1. 1) -RE_LIST_ITEM = re.compile( - r"^\s*(?:[-*•]|[–—]|(?:\d+)[.)])\s*(.+?)\s*$" -) +RE_LIST_ITEM = re.compile(r"^\s*(?:[-*•]|[–—]|(?:\d+)[.)])\s*(.+?)\s*$") # Regex to remove trailing "closing sentence" often generated by LLMs RE_TRAILING_CLOSURE = re.compile( - r"\n*All tags are closed and my assignment is finished\.\s*$", - flags=re.IGNORECASE, + r"\n*All tags are closed and my assignment is finished\.\s*$", flags=re.IGNORECASE ) @@ -47,7 +44,6 @@ class SubtaskConstraintAssignArgs(TypedDict): @final class _SubtaskConstraintAssign(PromptModule): - @staticmethod def _is_na_value(text: str) -> bool: """ @@ -72,8 +68,8 @@ def _extract_constraints(text: str) -> list[str]: - "• item" - "1. item" - "1) item" - - "– item" - - "— item" + - "en dash item" + - "em dash item" Returns: Deduplicated list while preserving order. @@ -174,9 +170,9 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: subtask_constraint_assign_match = re.search(RE_ASSIGNED_CONS, data[3]) if subtask_constraint_assign_match: - subtask_constraint_assign_str = ( - subtask_constraint_assign_match.group(1).strip() - ) + subtask_constraint_assign_str = subtask_constraint_assign_match.group( + 1 + ).strip() else: # Fallback to raw text if tags are missing FancyLogger.get_logger().warning( @@ -187,9 +183,7 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: # Remove common trailing LLM artifacts subtask_constraint_assign_str = re.sub( - RE_TRAILING_CLOSURE, - "", - subtask_constraint_assign_str, + RE_TRAILING_CLOSURE, "", subtask_constraint_assign_str ).strip() # Handle "no constraints" case @@ -198,8 +192,10 @@ def _default_parser(generated_str: str) -> list[SubtaskPromptConstraintsItem]: else: # Try extracting list-style constraints - subtask_constraint_assign = _SubtaskConstraintAssign._extract_constraints( - subtask_constraint_assign_str + subtask_constraint_assign = ( + _SubtaskConstraintAssign._extract_constraints( + subtask_constraint_assign_str + ) ) # -------- Secondary fallback -------- From 4f9b15a707c249ddb31e61b1796e68326a63b213 Mon Sep 17 00:00:00 2001 From: Bobby Date: Thu, 9 Apr 2026 12:56:33 -0400 Subject: [PATCH 26/26] clean: pre-commit test --- test/decompose/test_decompose.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/decompose/test_decompose.py b/test/decompose/test_decompose.py index b3297d5d0..35303a78b 100644 --- a/test/decompose/test_decompose.py +++ b/test/decompose/test_decompose.py @@ -292,9 +292,7 @@ def test_latest_template_writes_validation_modules_for_code_constraints( program = program_path.read_text(encoding="utf-8") assert "from mellea.stdlib.requirements import req" in program - assert ( - "from validations.val_fn_1 import validate_input as val_fn_1" in program - ) + assert "from validations.val_fn_1 import validate_input as val_fn_1" in program assert "Keep the answer concise." in program def test_multi_line_input_file_creates_numbered_jobs(