From c0c539086c47c6d60ca7809b2d303c65b16faf42 Mon Sep 17 00:00:00 2001 From: lxasqjc Date: Tue, 7 Oct 2025 09:14:43 +0100 Subject: [PATCH 1/2] feat: Add human-in-the-loop interactive mode for Biomni agents - Add interactive parameter to A1 class for human confirmation workflow - Implement _get_human_confirmation() method with approve/edit/reject options - Add plan editing capabilities with multi-line input support - Display interactive mode status in agent configuration - Maintain backward compatibility with non-interactive mode (default) - Add comprehensive documentation with usage examples and API reference --- biomni/agent/a1.py | 190 +++++++++++++++++++++++++++--- docs/HUMAN_IN_THE_LOOP.md | 241 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 417 insertions(+), 14 deletions(-) create mode 100644 docs/HUMAN_IN_THE_LOOP.md diff --git a/biomni/agent/a1.py b/biomni/agent/a1.py index df151e4e4..f2f8cc171 100644 --- a/biomni/agent/a1.py +++ b/biomni/agent/a1.py @@ -64,6 +64,7 @@ def __init__( api_key: str | None = None, commercial_mode: bool | None = None, expected_data_lake_files: list | None = None, + interactive: bool = False, ): """Initialize the biomni agent. @@ -76,6 +77,7 @@ def __init__( base_url: Base URL for custom model serving (e.g., "http://localhost:8000/v1") api_key: API key for the custom LLM commercial_mode: If True, excludes datasets that require commercial licenses or are non-commercial only + interactive: If True, enables human-in-the-loop mode with plan confirmation and editing capabilities """ # Use default_config values for unspecified parameters @@ -143,6 +145,13 @@ def __init__( if api_key is not None and api_key != "EMPTY": print(f" API Key: {'*' * 8 + api_key[-4:] if len(api_key) > 8 else '***'}") + # Show interactive mode status + if interactive: + print("\n๐Ÿค INTERACTIVE MODE: Enabled") + print(" โ€ข Human-in-the-loop confirmation for code execution") + print(" โ€ข Plan editing capabilities before execution") + print(" โ€ข User control over agent decisions") + print("=" * 50 + "\n") self.path = path @@ -207,6 +216,9 @@ def __init__( # Add timeout parameter self.timeout_seconds = timeout_seconds # 10 minutes default timeout + + # Add interactive mode parameter for human-in-the-loop functionality + self.interactive = interactive self.configure() def add_tool(self, api): @@ -1307,7 +1319,68 @@ def generate(state: AgentState) -> AgentState: execute_match = re.search(r"(.*?)", msg, re.DOTALL) answer_match = re.search(r"(.*?)", msg, re.DOTALL) - # Add the message to the state before checking for errors + # Human-in-the-loop: Get confirmation before proceeding + if self.interactive and (execute_match or answer_match): + try: + if execute_match: + code_to_execute = execute_match.group(1).strip() + approved, modified_code = self._get_human_confirmation( + code_to_execute, "code execution" + ) + + if not approved: + # User rejected the plan, ask agent to regenerate + state["messages"].append( + HumanMessage( + content="The proposed code execution was not approved. Please revise your approach and provide a different solution." + ) + ) + state["next_step"] = "generate" + return state + + # If user modified the code, update the message + if modified_code != code_to_execute: + msg = re.sub( + r"(.*?)", + f"{modified_code}", + msg, + flags=re.DOTALL + ) + + elif answer_match: + solution = answer_match.group(1).strip() + approved, modified_solution = self._get_human_confirmation( + solution, "final solution" + ) + + if not approved: + # User rejected the solution, ask agent to regenerate + state["messages"].append( + HumanMessage( + content="The proposed solution was not approved. Please revise your analysis and provide a different solution." + ) + ) + state["next_step"] = "generate" + return state + + # If user modified the solution, update the message + if modified_solution != solution: + msg = re.sub( + r"(.*?)", + f"{modified_solution}", + msg, + flags=re.DOTALL + ) + + except KeyboardInterrupt: + # User stopped execution + state["messages"].append( + AIMessage(content="Execution stopped by user request.") + ) + state["next_step"] = "end" + return state + + # Add the message to the state after potential human modifications state["messages"].append(AIMessage(content=msg.strip())) if answer_match: @@ -1600,6 +1673,10 @@ def go(self, prompt): prompt: The user's query """ + if self.interactive: + print(f"\n๐Ÿค Starting INTERACTIVE mode - You'll be asked to confirm plans before execution") + print(f"๐Ÿ’ก Tip: You can approve, edit, reject, or stop execution at any confirmation point\n") + self.critic_count = 0 self.user_task = prompt @@ -1614,11 +1691,16 @@ def go(self, prompt): # Store the final conversation state for markdown generation final_state = None - for s in self.app.stream(inputs, stream_mode="values", config=config): - message = s["messages"][-1] - out = pretty_print(message) - self.log.append(out) - final_state = s # Store the latest state + try: + for s in self.app.stream(inputs, stream_mode="values", config=config): + message = s["messages"][-1] + out = pretty_print(message) + self.log.append(out) + final_state = s # Store the latest state + except KeyboardInterrupt: + if self.interactive: + print("\n๐Ÿ›‘ Execution interrupted by user.") + raise # Store the conversation state for markdown generation self._conversation_state = final_state @@ -1637,6 +1719,10 @@ def go_stream(self, prompt) -> Generator[dict, None, None]: Yields: dict: Each step of the agent's execution containing the current message and state """ + if self.interactive: + print(f"\n๐Ÿค Starting INTERACTIVE streaming mode - You'll be asked to confirm plans before execution") + print(f"๐Ÿ’ก Tip: You can approve, edit, reject, or stop execution at any confirmation point\n") + self.critic_count = 0 self.user_task = prompt @@ -1651,14 +1737,19 @@ def go_stream(self, prompt) -> Generator[dict, None, None]: # Store the final conversation state for markdown generation final_state = None - for s in self.app.stream(inputs, stream_mode="values", config=config): - message = s["messages"][-1] - out = pretty_print(message) - self.log.append(out) - final_state = s # Store the latest state - - # Yield the current step - yield {"output": out} + try: + for s in self.app.stream(inputs, stream_mode="values", config=config): + message = s["messages"][-1] + out = pretty_print(message) + self.log.append(out) + final_state = s # Store the latest state + + # Yield the current step + yield {"output": out} + except KeyboardInterrupt: + if self.interactive: + print("\n๐Ÿ›‘ Streaming execution interrupted by user.") + raise # Store the conversation state for markdown generation self._conversation_state = final_state @@ -2353,6 +2444,77 @@ def _convert_markdown_to_pdf(self, markdown_path: str, pdf_path: str) -> None: """ convert_markdown_to_pdf(markdown_path, pdf_path) + def _get_human_confirmation(self, plan: str, plan_type: str = "plan") -> tuple[bool, str]: + """Get human confirmation for a generated plan with optional editing. + + Args: + plan: The generated plan to confirm/edit + plan_type: Type of plan (e.g., "plan", "code", "analysis") + + Returns: + tuple: (approved, modified_plan) - True if approved, and the potentially modified plan + """ + print(f"\n{'='*60}") + print(f"๐Ÿค– BIOMNI AGENT - {plan_type.upper()} CONFIRMATION") + print(f"{'='*60}") + print(f"\n๐Ÿ“‹ Generated {plan_type}:") + print(f"{'-'*40}") + print(plan) + print(f"{'-'*40}") + + while True: + print(f"\n๐Ÿค” What would you like to do?") + print(" 1. โœ… Approve and proceed") + print(" 2. โœ๏ธ Edit the plan") + print(" 3. โŒ Reject and ask agent to regenerate") + print(" 4. ๐Ÿ›‘ Stop execution") + + choice = input("\nEnter your choice (1-4): ").strip() + + if choice == "1": + print("โœ… Plan approved! Proceeding with execution...") + return True, plan + + elif choice == "2": + print(f"\nโœ๏ธ Edit mode - Current {plan_type}:") + print(f"{'-'*40}") + print(plan) + print(f"{'-'*40}") + print("\nEnter your modifications (press Enter twice to finish):") + + lines = [] + empty_count = 0 + while empty_count < 2: + line = input() + if line == "": + empty_count += 1 + else: + empty_count = 0 + lines.append(line) + + # Remove trailing empty lines + while lines and lines[-1] == "": + lines.pop() + + modified_plan = "\n".join(lines) + if modified_plan.strip(): + print("โœ… Plan updated! Proceeding with modified version...") + return True, modified_plan + else: + print("โŒ Empty modification. Keeping original plan.") + continue + + elif choice == "3": + print("โŒ Plan rejected. Asking agent to regenerate...") + return False, plan + + elif choice == "4": + print("๐Ÿ›‘ Execution stopped by user.") + raise KeyboardInterrupt("Execution stopped by user request") + + else: + print("โŒ Invalid choice. Please enter 1, 2, 3, or 4.") + def _clear_execution_plots(self): """Clear execution plots before new execution. diff --git a/docs/HUMAN_IN_THE_LOOP.md b/docs/HUMAN_IN_THE_LOOP.md new file mode 100644 index 000000000..e95d21ab8 --- /dev/null +++ b/docs/HUMAN_IN_THE_LOOP.md @@ -0,0 +1,241 @@ +# Human-in-the-Loop Mode for Biomni Agent + +The Biomni agent now supports **Human-in-the-Loop (HITL)** functionality, allowing users to interactively confirm, edit, or reject the agent's plans before execution. This feature provides enhanced control and oversight over the agent's decision-making process. + +## ๐ŸŽฏ Key Features + +- **Interactive Plan Confirmation**: Review and approve code execution and final solutions +- **Real-time Plan Editing**: Modify the agent's proposed code or solutions before execution +- **Flexible Control**: Approve, edit, reject, or stop execution at any point +- **Safety & Oversight**: Prevent unwanted actions through human confirmation +- **Educational Value**: Understand the agent's reasoning and learn from its approach + +## ๐Ÿš€ Usage + +### Basic Interactive Mode + +```python +from biomni.agent.a1 import A1 + +# Create an agent with interactive mode enabled +agent = A1( + path="./data", + llm="gpt-4o-mini", + source="OpenAI", + interactive=True # Enable human-in-the-loop mode +) + +# Execute a query - you'll be prompted for confirmation +log, response = agent.go("Create a scatter plot of random data") +``` + +### Non-Interactive Mode (Default) + +```python +# Standard agent behavior - automatic execution +agent = A1( + path="./data", + llm="gpt-4o-mini", + source="OpenAI", + interactive=False # Default: no human confirmation +) + +log, response = agent.go("Create a scatter plot of random data") # Executes automatically +``` + +## ๐Ÿค Interactive Workflow + +When interactive mode is enabled, the agent will pause before: + +1. **Code Execution**: Before running any `` blocks +2. **Final Solutions**: Before providing `` responses + +At each confirmation point, you have four options: + +### 1. โœ… Approve and Proceed +- Accepts the proposed plan as-is +- Continues with execution + +### 2. โœ๏ธ Edit the Plan +- Allows you to modify the code or solution +- Opens an editor interface for making changes +- Proceeds with your modified version + +### 3. โŒ Reject and Regenerate +- Rejects the current plan +- Asks the agent to generate an alternative approach +- Useful when the approach isn't suitable + +### 4. ๐Ÿ›‘ Stop Execution +- Immediately stops the agent +- Useful for safety or when you want to start over + +## ๐Ÿ“‹ Example Interactive Session + +``` +๐Ÿค– BIOMNI AGENT - CODE EXECUTION CONFIRMATION +============================================================ + +๐Ÿ“‹ Generated code execution: +---------------------------------------- +import matplotlib.pyplot as plt +import numpy as np + +# Generate random data +x = np.random.randn(100) +y = np.random.randn(100) + +# Create scatter plot +plt.scatter(x, y) +plt.title('Random Data Scatter Plot') +plt.xlabel('X values') +plt.ylabel('Y values') +plt.show() +---------------------------------------- + +๐Ÿค” What would you like to do? + 1. โœ… Approve and proceed + 2. โœ๏ธ Edit the plan + 3. โŒ Reject and ask agent to regenerate + 4. ๐Ÿ›‘ Stop execution + +Enter your choice (1-4): 2 + +โœ๏ธ Edit mode - Current code execution: +---------------------------------------- +import matplotlib.pyplot as plt +import numpy as np + +# Generate random data +x = np.random.randn(100) +y = np.random.randn(100) + +# Create scatter plot +plt.scatter(x, y) +plt.title('Random Data Scatter Plot') +plt.xlabel('X values') +plt.ylabel('Y values') +plt.show() +---------------------------------------- + +Enter your modifications (press Enter twice to finish): +# Add color and transparency +plt.scatter(x, y, alpha=0.6, c='blue') +plt.grid(True) + + +โœ… Plan updated! Proceeding with modified version... +``` + +## ๐ŸŽ“ Use Cases + +### Research & Analysis +- **Exploratory Data Analysis**: Review analysis steps before execution +- **Scientific Computing**: Ensure computational approaches are appropriate +- **Data Visualization**: Fine-tune plots and charts before generation + +### Educational Purposes +- **Learning AI Reasoning**: See how the agent approaches problems +- **Code Review**: Understand and improve generated code +- **Best Practices**: Learn from AI-generated solutions + +### Production & Safety +- **Mission-Critical Tasks**: Human oversight for important decisions +- **Data Security**: Review data access and manipulation +- **Compliance**: Ensure adherence to organizational policies + +### Collaborative Development +- **Domain Expertise**: Inject specialized knowledge into AI workflows +- **Quality Assurance**: Review outputs before finalization +- **Iterative Refinement**: Gradually improve solutions through feedback + +## โš™๏ธ Configuration Options + +The interactive mode is controlled by the `interactive` parameter in the A1 constructor: + +```python +agent = A1( + path="./data", + llm="gpt-4o-mini", + source="OpenAI", + interactive=True, # Enable/disable interactive mode + use_tool_retriever=True, + timeout_seconds=600, + commercial_mode=False +) +``` + +When interactive mode is enabled, you'll see additional configuration output: + +``` +๐Ÿค INTERACTIVE MODE: Enabled + โ€ข Human-in-the-loop confirmation for code execution + โ€ข Plan editing capabilities before execution + โ€ข User control over agent decisions +``` + +## ๐Ÿ”ง Integration with Existing Workflows + +### Streaming Mode +Interactive mode works with both `go()` and `go_stream()` methods: + +```python +# Interactive streaming +for step in agent.go_stream("Analyze this dataset"): + print(step["output"]) + # Confirmation prompts will appear during streaming +``` + +### Error Handling +Interactive mode gracefully handles interruptions: + +```python +try: + log, response = agent.go("Complex analysis task") +except KeyboardInterrupt: + print("User stopped execution") +``` + +### Non-Interactive Fallback +Easily switch between modes for different use cases: + +```python +# Interactive for development +dev_agent = A1(interactive=True, ...) +log, response = dev_agent.go("Prototype solution") + +# Non-interactive for production +prod_agent = A1(interactive=False, ...) +log, response = prod_agent.go("Production analysis") +``` + +## ๐Ÿ”ฎ Future Integration + +This human-in-the-loop implementation is designed for easy integration with: + +- **OpenWebUI**: Web-based confirmation interfaces +- **Langflow**: Visual workflow builders with approval nodes +- **Custom UIs**: RESTful APIs for confirmation endpoints +- **Chat Interfaces**: Interactive messaging platforms + +The modular design allows the confirmation logic to be easily replaced with web-based or API-driven alternatives while maintaining the same core functionality. + +## ๐Ÿงช Testing + +Run the test suite to verify human-in-the-loop functionality: + +```bash +# Automated tests +python tests/test_human_in_the_loop.py + +# Interactive example +python examples/human_in_the_loop_example.py +``` + +## ๐Ÿ’ก Tips + +1. **Start Simple**: Begin with basic queries to understand the confirmation flow +2. **Edit Incrementally**: Make small changes during editing to avoid errors +3. **Use Rejection Wisely**: Reject plans when the approach is fundamentally wrong +4. **Learn from Patterns**: Observe how the agent approaches different problem types +5. **Combine Modes**: Use interactive for development, non-interactive for production \ No newline at end of file From 8a23551df9a7f79ff8c14f4a68f60c3ffc0a70c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 08:18:28 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- biomni/agent/a1.py | 83 +++++++++++++++++---------------------- docs/HUMAN_IN_THE_LOOP.md | 10 ++--- 2 files changed, 42 insertions(+), 51 deletions(-) diff --git a/biomni/agent/a1.py b/biomni/agent/a1.py index f2f8cc171..fc5d2a71f 100644 --- a/biomni/agent/a1.py +++ b/biomni/agent/a1.py @@ -216,7 +216,7 @@ def __init__( # Add timeout parameter self.timeout_seconds = timeout_seconds # 10 minutes default timeout - + # Add interactive mode parameter for human-in-the-loop functionality self.interactive = interactive self.configure() @@ -1324,10 +1324,8 @@ def generate(state: AgentState) -> AgentState: try: if execute_match: code_to_execute = execute_match.group(1).strip() - approved, modified_code = self._get_human_confirmation( - code_to_execute, "code execution" - ) - + approved, modified_code = self._get_human_confirmation(code_to_execute, "code execution") + if not approved: # User rejected the plan, ask agent to regenerate state["messages"].append( @@ -1337,22 +1335,17 @@ def generate(state: AgentState) -> AgentState: ) state["next_step"] = "generate" return state - + # If user modified the code, update the message if modified_code != code_to_execute: msg = re.sub( - r"(.*?)", - f"{modified_code}", - msg, - flags=re.DOTALL + r"(.*?)", f"{modified_code}", msg, flags=re.DOTALL ) - + elif answer_match: solution = answer_match.group(1).strip() - approved, modified_solution = self._get_human_confirmation( - solution, "final solution" - ) - + approved, modified_solution = self._get_human_confirmation(solution, "final solution") + if not approved: # User rejected the solution, ask agent to regenerate state["messages"].append( @@ -1362,21 +1355,19 @@ def generate(state: AgentState) -> AgentState: ) state["next_step"] = "generate" return state - + # If user modified the solution, update the message if modified_solution != solution: msg = re.sub( r"(.*?)", f"{modified_solution}", msg, - flags=re.DOTALL + flags=re.DOTALL, ) - + except KeyboardInterrupt: # User stopped execution - state["messages"].append( - AIMessage(content="Execution stopped by user request.") - ) + state["messages"].append(AIMessage(content="Execution stopped by user request.")) state["next_step"] = "end" return state @@ -1674,9 +1665,9 @@ def go(self, prompt): """ if self.interactive: - print(f"\n๐Ÿค Starting INTERACTIVE mode - You'll be asked to confirm plans before execution") - print(f"๐Ÿ’ก Tip: You can approve, edit, reject, or stop execution at any confirmation point\n") - + print("\n๐Ÿค Starting INTERACTIVE mode - You'll be asked to confirm plans before execution") + print("๐Ÿ’ก Tip: You can approve, edit, reject, or stop execution at any confirmation point\n") + self.critic_count = 0 self.user_task = prompt @@ -1720,9 +1711,9 @@ def go_stream(self, prompt) -> Generator[dict, None, None]: dict: Each step of the agent's execution containing the current message and state """ if self.interactive: - print(f"\n๐Ÿค Starting INTERACTIVE streaming mode - You'll be asked to confirm plans before execution") - print(f"๐Ÿ’ก Tip: You can approve, edit, reject, or stop execution at any confirmation point\n") - + print("\n๐Ÿค Starting INTERACTIVE streaming mode - You'll be asked to confirm plans before execution") + print("๐Ÿ’ก Tip: You can approve, edit, reject, or stop execution at any confirmation point\n") + self.critic_count = 0 self.user_task = prompt @@ -2446,42 +2437,42 @@ def _convert_markdown_to_pdf(self, markdown_path: str, pdf_path: str) -> None: def _get_human_confirmation(self, plan: str, plan_type: str = "plan") -> tuple[bool, str]: """Get human confirmation for a generated plan with optional editing. - + Args: plan: The generated plan to confirm/edit plan_type: Type of plan (e.g., "plan", "code", "analysis") - + Returns: tuple: (approved, modified_plan) - True if approved, and the potentially modified plan """ - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(f"๐Ÿค– BIOMNI AGENT - {plan_type.upper()} CONFIRMATION") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"\n๐Ÿ“‹ Generated {plan_type}:") - print(f"{'-'*40}") + print(f"{'-' * 40}") print(plan) - print(f"{'-'*40}") - + print(f"{'-' * 40}") + while True: - print(f"\n๐Ÿค” What would you like to do?") + print("\n๐Ÿค” What would you like to do?") print(" 1. โœ… Approve and proceed") print(" 2. โœ๏ธ Edit the plan") print(" 3. โŒ Reject and ask agent to regenerate") print(" 4. ๐Ÿ›‘ Stop execution") - + choice = input("\nEnter your choice (1-4): ").strip() - + if choice == "1": print("โœ… Plan approved! Proceeding with execution...") return True, plan - + elif choice == "2": print(f"\nโœ๏ธ Edit mode - Current {plan_type}:") - print(f"{'-'*40}") + print(f"{'-' * 40}") print(plan) - print(f"{'-'*40}") + print(f"{'-' * 40}") print("\nEnter your modifications (press Enter twice to finish):") - + lines = [] empty_count = 0 while empty_count < 2: @@ -2491,11 +2482,11 @@ def _get_human_confirmation(self, plan: str, plan_type: str = "plan") -> tuple[b else: empty_count = 0 lines.append(line) - + # Remove trailing empty lines while lines and lines[-1] == "": lines.pop() - + modified_plan = "\n".join(lines) if modified_plan.strip(): print("โœ… Plan updated! Proceeding with modified version...") @@ -2503,15 +2494,15 @@ def _get_human_confirmation(self, plan: str, plan_type: str = "plan") -> tuple[b else: print("โŒ Empty modification. Keeping original plan.") continue - + elif choice == "3": print("โŒ Plan rejected. Asking agent to regenerate...") return False, plan - + elif choice == "4": print("๐Ÿ›‘ Execution stopped by user.") raise KeyboardInterrupt("Execution stopped by user request") - + else: print("โŒ Invalid choice. Please enter 1, 2, 3, or 4.") diff --git a/docs/HUMAN_IN_THE_LOOP.md b/docs/HUMAN_IN_THE_LOOP.md index e95d21ab8..e9d407f56 100644 --- a/docs/HUMAN_IN_THE_LOOP.md +++ b/docs/HUMAN_IN_THE_LOOP.md @@ -35,7 +35,7 @@ log, response = agent.go("Create a scatter plot of random data") # Standard agent behavior - automatic execution agent = A1( path="./data", - llm="gpt-4o-mini", + llm="gpt-4o-mini", source="OpenAI", interactive=False # Default: no human confirmation ) @@ -170,7 +170,7 @@ When interactive mode is enabled, you'll see additional configuration output: ``` ๐Ÿค INTERACTIVE MODE: Enabled โ€ข Human-in-the-loop confirmation for code execution - โ€ข Plan editing capabilities before execution + โ€ข Plan editing capabilities before execution โ€ข User control over agent decisions ``` @@ -204,7 +204,7 @@ Easily switch between modes for different use cases: dev_agent = A1(interactive=True, ...) log, response = dev_agent.go("Prototype solution") -# Non-interactive for production +# Non-interactive for production prod_agent = A1(interactive=False, ...) log, response = prod_agent.go("Production analysis") ``` @@ -235,7 +235,7 @@ python examples/human_in_the_loop_example.py ## ๐Ÿ’ก Tips 1. **Start Simple**: Begin with basic queries to understand the confirmation flow -2. **Edit Incrementally**: Make small changes during editing to avoid errors +2. **Edit Incrementally**: Make small changes during editing to avoid errors 3. **Use Rejection Wisely**: Reject plans when the approach is fundamentally wrong 4. **Learn from Patterns**: Observe how the agent approaches different problem types -5. **Combine Modes**: Use interactive for development, non-interactive for production \ No newline at end of file +5. **Combine Modes**: Use interactive for development, non-interactive for production