From e5e11a1196951594ba2d72eb3c2ca0e343a3043f Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 3 Jun 2025 18:29:32 -0500 Subject: [PATCH 001/172] upload beam search and beam search history algorithms --- examples/example_usage_trainer.py | 360 ++++++++ .../algorithms/beamsearch_algorithm.py | 781 ++++++++++++++++++ 2 files changed, 1141 insertions(+) create mode 100644 examples/example_usage_trainer.py create mode 100644 opto/trainer/algorithms/beamsearch_algorithm.py diff --git a/examples/example_usage_trainer.py b/examples/example_usage_trainer.py new file mode 100644 index 00000000..78ff2793 --- /dev/null +++ b/examples/example_usage_trainer.py @@ -0,0 +1,360 @@ +# Standard library imports +import os +import time +import argparse +from typing import Any, Dict, List, Optional, Tuple, Union + +# Third-party imports +import datasets +import numpy as np + +# Opto imports +from opto import trace +from opto.optimizers import OptoPrime +from opto.optimizers.utils import print_color +from opto.trace.modules import Module +from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, BasicSearchAlgorithm +from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm +from opto.trainer.guide import AutoGuide +from opto.trainer.utils import DefaultLogger +from opto.utils.llm import LLM, LiteLLM + +# Set default model +os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" + +@trace.model +class Learner(Module): + """A basic LLM Agent for solving math problems.""" + + def __init__(self, + system_prompt: str = "You're a helpful agent answering math problems.", + user_prompt_template: str = "Solve the following math problem step-by-step: {message}", + llm: LLM = None): + """Initialize the learner agent. + + Args: + system_prompt: System prompt to guide LLM behavior + user_prompt_template: Template for formatting user messages + llm: LLM instance to use for generation (defaults to gpt-3.5-turbo) + """ + super().__init__() + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template, trainable=True) + self.llm = llm or LiteLLM(model="gpt-3.5-turbo") + + @trace.bundle() + def call_llm(self, system_prompt: str, user_prompt: str) -> str: + """Call LLM model with the given prompts. + + Args: + system_prompt: The system prompt + user_prompt: The user prompt + + Returns: + The LLM response content + """ + response = self.llm( + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ] + ) + return response.choices[0].message.content + + def forward(self, message: Any) -> str: + """Agent's forward pass to process a message. + + Args: + message: The input message to process + + Returns: + The generated response + """ + user_prompt = self.user_prompt_template.format(message=message) + return self.call_llm(self.system_prompt, user_prompt) + + +class TeacherGuide(AutoGuide): + """Guide that uses LLM to judge answers and provide feedback.""" + + def __init__(self, model: str = "gpt-4o-mini"): + """Initialize the teacher guide. + + Args: + model: The LLM model to use for evaluation + """ + super().__init__() + self.guide_llm = LiteLLM(model=model) + self.system_prompt = "You are an expert math teacher evaluating student answers." + self.judge_prompt_template = ( + "Carefully review the following three distinct sections:\n\n" + "SECTION 1: The Math Problem\n" + "----------------------------\n" + "{query}\n" + "----------------------------\n\n" + "SECTION 2: The Student's Full Answer\n" + "----------------------------\n" + "{response}\n" + "----------------------------\n\n" + "SECTION 3: The Official Correct Answer\n" + "----------------------------\n" + "{reference}\n" + "----------------------------\n\n" + "INSTRUCTIONS FOR JUDGING:\n" + "1. Your primary task is to compare the student's **final numerical result** (or final conclusion if no number is present) from SECTION 2 with the **Official Correct Answer** provided in SECTION 3.\n" + "2. When evaluating SECTION 2 (Student's Full Answer), focus SOLELY on the **final answer part** of the student's response. Ignore all intermediate steps, reasoning, or explanations for the correctness check unless the problem specifically asks for reasoning as the final answer.\n" + "3. Determine if the student's **final answer** is equivalent to the **Official Correct Answer**.\n\n" + "RESPONSE FORMAT:\n" + "- If the student's final answer (from SECTION 2) IS equivalent to the Official Correct Answer (from SECTION 3), respond ONLY with the exact phrase: 'Correct [TERMINATE]'\n" + "- If the student's final answer IS NOT equivalent, respond ONLY with specific and actionable feedback. The feedback should clearly explain the error in the student's final answer and guide them on how to arrive at the Official Correct Answer." + ) + + def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]: + """Get feedback on a student response. + + Args: + task: The original math problem + response: The student's answer + info: The reference/correct answer + **kwargs: Additional arguments + + Returns: + Tuple of (score, feedback_text) + """ + user_prompt = self.judge_prompt_template.format( + query=task, + response=response, + reference=info + ) + + messages = [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": user_prompt} + ] + + llm_response = self.guide_llm(messages=messages) + feedback_text = llm_response.choices[0].message.content + + if 'Correct [TERMINATE]' in feedback_text: + return 1.0, "Correct." + else: + return 0.0, f"Incorrect. Feedback: {feedback_text}" + + def metric(self, task: str, content: str, info: Any, **kwargs) -> float: + """Calculate the metric score for an answer. + + Args: + task: The original math problem + content: The student's answer + info: The reference/correct answer + **kwargs: Additional arguments + + Returns: + Score (0.0 or 1.0) + """ + score, _ = self.get_feedback(task, content, info, **kwargs) + return score + + +class SimpleLogger(DefaultLogger): + """Simplified logger that only shows important metrics.""" + + def log(self, name: str, data: Any, step: int, **kwargs): + """Log only specific metrics to reduce output clutter. + + Args: + name: The name of the metric + data: The metric value + step: The current step + **kwargs: Additional logging arguments + """ + important_metrics = [ + 'Average train score', + 'Average test score', + 'Validation score' + ] + + if name in important_metrics or 'Parameter' in name: + super().log(name, data, step, **kwargs) + + +def main(): + """Run the main training process with command line arguments.""" + parser = argparse.ArgumentParser(description='Train agent using various algorithms') + + # Algorithm parameters + parser.add_argument('--algorithm_type', type=str, default='beamsearchhistory', + choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory'], + help='Type of algorithm to use') + + # Dataset parameters + parser.add_argument('--dataset', type=str, default='xuanfeiren/math_hard_gemini', + help='Dataset to use for training') + parser.add_argument('--num_train_samples', type=int, default=66, + help='Number of training samples') + parser.add_argument('--num_validate_samples', type=int, default=20, + help='Number of validation samples') + parser.add_argument('--num_test_samples', type=int, default=1, + help='Number of test samples') + + # Model parameters + parser.add_argument('--trace_model', type=str, default='vertex_ai/gemini-2.0-flash', + help='Model to use for trace operations') + parser.add_argument('--student_model', type=str, default='vertex_ai/gemini-2.0-flash', + help='Model to use for student agent') + parser.add_argument('--teacher_model', type=str, default='vertex_ai/gemini-2.0-flash', + help='Model to use for teacher guide') + + # Training parameters + parser.add_argument('--num_epochs', type=int, default=1, + help='Number of training epochs') + parser.add_argument('--batch_size', type=int, default=2, + help='Training batch size') + parser.add_argument('--num_threads', type=int, default=20, + help='Number of threads for parallel processing') + parser.add_argument('--eval_frequency', type=int, default=2, + help='How often to run evaluation') + parser.add_argument('--log_frequency', type=int, default=20, + help='How often to log results') + parser.add_argument('--seed', type=int, default=42, + help='Random seed for reproducibility') + + # Algorithm-specific parameters + parser.add_argument('--beam_width', type=int, default=2, + help='Beam width for beam search algorithms') + parser.add_argument('--num_proposals', type=int, default=2, + help='Number of proposals for beam search algorithms') + parser.add_argument('--max_depth', type=int, default=5, + help='Maximum depth for beam search algorithms') + parser.add_argument('--validation_dataset_size', type=int, default=20, + help='Size of validation dataset for beam search') + parser.add_argument('--max_history_size', type=int, default=12, + help='Maximum history size for history-based algorithms') + parser.add_argument('--num_basicsearch_proposals', type=int, default=2, + help='Number of proposals for basic search algorithm') + + args = parser.parse_args() + + # Set environment variables + os.environ["TRACE_LITELLM_MODEL"] = args.trace_model + + # Set random seed + np.random.seed(args.seed) + + # Check for API Keys + if not os.getenv("OPENAI_API_KEY") and not os.getenv("ANTHROPIC_API_KEY"): + print_color("Warning: OPENAI_API_KEY or ANTHROPIC_API_KEY environment variables not found. LLM calls may fail.", "red") + + # Load and prepare data + print(f"Loading data from {args.dataset}...") + math_data = datasets.load_dataset(args.dataset) + + # Select data subsets + train_data = math_data['train'].select( + range(args.num_train_samples, args.num_train_samples + args.num_validate_samples) + ) + validate_data = train_data + test_data = math_data['test'].select(range(args.num_test_samples)) + + # Format data for trainer + train_dataset = {'inputs': train_data['problem'], 'infos': train_data['solution']} + validate_dataset = {'inputs': validate_data['problem'], 'infos': validate_data['solution']} + test_dataset = {'inputs': test_data['problem'], 'infos': test_data['solution']} + + # Log dataset sizes + print(f"Training samples: {len(train_dataset['inputs'])}") + print(f"Validation samples: {len(validate_dataset['inputs'])}") + print(f"Test samples: {len(test_dataset['inputs'])}") + + # Initialize components + print("Initializing Agent, Guide, Optimizer, Algorithm...") + student_llm = LiteLLM(model=args.student_model) + agent = Learner(llm=student_llm) + + train_guide = TeacherGuide(model=args.teacher_model) + validate_guide = TeacherGuide(model=args.teacher_model) + + optimizer = OptoPrime(agent.parameters()) + logger = SimpleLogger() + + # Create algorithm + if args.algorithm_type == 'minibatch': + algorithm = MinibatchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads + ) + elif args.algorithm_type == 'basicsearch': + algorithm = BasicSearchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads + ) + elif args.algorithm_type == 'beamsearch': + algorithm = BeamsearchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads + ) + elif args.algorithm_type == 'beamsearchhistory': + algorithm = BeamsearchHistoryAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads + ) + else: + raise ValueError(f"Unknown algorithm type: {args.algorithm_type}") + + # Prepare training parameters + train_params = { + "guide": train_guide, + "train_dataset": train_dataset, + "num_epochs": args.num_epochs, + "num_threads": args.num_threads, + "batch_size": args.batch_size, + "test_dataset": test_dataset, + "validate_dataset": validate_dataset, + "validate_guide": validate_guide, + "eval_frequency": args.eval_frequency, + "log_frequency": args.log_frequency, + "validation_dataset_size": args.validation_dataset_size, + } + + # Add algorithm-specific parameters + if args.algorithm_type in ['beamsearch', 'beamsearchhistory']: + train_params.update({ + "beam_width": args.beam_width, + "num_proposals": args.num_basicsearch_proposals, + "max_depth": args.max_depth + }) + + if args.algorithm_type == 'beamsearchhistory': + train_params["max_history_size"] = args.max_history_size + + elif args.algorithm_type == 'basicsearch': + train_params["num_proposals"] = args.num_basicsearch_proposals + + # Start training + print(f"Training with {args.algorithm_type} algorithm...") + start_time = time.time() + metrics, final_score = algorithm.train(**train_params) + duration = time.time() - start_time + print(f"Training complete, time taken: {duration:.2f} seconds") + + # Print metrics summary based on algorithm type + if args.algorithm_type in ['beamsearch', 'beamsearchhistory'] and 'best_validation_scores' in metrics: + print("\nBest validation scores at each depth:") + for depth, score in enumerate(metrics['best_validation_scores']): + print(f" Depth {depth+1}: {score:.4f}") + + print(f"Final score: {final_score:.4f}") + + return metrics, final_score + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py new file mode 100644 index 00000000..2d63f5a1 --- /dev/null +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -0,0 +1,781 @@ +import numpy as np +import copy +from typing import Union, List, Tuple, Dict, Any, Optional +from opto.trainer.utils import async_run +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate, batchify + + +class BeamsearchAlgorithm(MinibatchAlgorithm): + """ + BeamsearchAlgorithm performs beam search over parameter space. + """ """ + It starts with an initial prompt, generates multiple candidates, + selects top beam_width candidates, and repeats this process up to max_depth. + At each step, it evaluates candidates on a validation set to select the best ones. + Finally output the best candidate based on validation scores. + """ + + def train(self, + guide, + train_dataset, + *, + validate_dataset=None, # dataset for selecting the best candidates + validate_guide=None, # guide for validation + validation_dataset_size=5, # size of validation minibatch for each evaluation + beam_width=3, # number of candidates to keep at each beam step + num_proposals=4, # number of proposals to generate per beam + max_depth=2, # maximum depth of beam search + num_epochs=1, + batch_size=1, + test_dataset=None, + log_frequency=None, + save_frequency=None, + save_path="checkpoints/agent.pkl", + min_score=None, + num_threads=10, + test_frequency=4, # How often to evaluate on test set + **kwargs + ): + """ + Performs beam search to find optimal parameters. + + Args: + beam_width: Number of candidates to keep at each level of the beam search + num_proposals: Number of proposals to generate per beam candidate + max_depth: Maximum depth of the beam search + validate_dataset: Dataset used to select the best candidates + validate_guide: Guide used for validation + validation_dataset_size: Size of validation minibatch for each evaluation (if None, uses all) + test_frequency: How often to evaluate on test set (every N steps) + Other parameters are the same as MinibatchAlgorithm.train() + """ + self.total_samples = 0 + + print_color(f"Running BeamsearchAlgorithm with beam_width={beam_width}, max_depth={max_depth}", 'blue') + + # Use train dataset for validation if not specified + validate_dataset = validate_dataset or train_dataset + validate_guide = validate_guide or guide + self.min_score = min_score + + # Default validation dataset size + if validation_dataset_size is None: + # Use a reasonable default - e.g., 10 samples or all if dataset is smaller + validation_dataset_size = min(10, len(validate_dataset['inputs'])) + + print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') + + # Store original parameters to restore after each exploration + original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + # Dictionary to track metrics during beam search + metrics = { + 'best_validation_scores': [], # Best validation score at each depth + 'depth_scores': [], # All scores at each depth + 'test_scores': [], # Test scores at periodic intervals + 'test_depths': [] # Depths at which test scores were recorded + } + + # Evaluate initial parameters on test set + if test_dataset is not None: + print_color("\n===== Evaluating Initial Parameters =====", 'blue') + initial_test_scores = evaluate( + self.agent, + guide, + test_dataset['inputs'], + test_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description="Evaluating initial parameters on test set" + ) + initial_test_score = np.mean(initial_test_scores) if all([s is not None for s in initial_test_scores]) else -np.inf + print_color(f"Initial test score: {initial_test_score:.4f}", 'yellow') + + # Add initial score to metrics for logging + metrics['test_scores'].append(initial_test_score) + metrics['test_depths'].append(1) # Represent initial score at depth 0 + + # Start with a single beam (the original parameters) + beams = [original_params] + + # Run beam search for max_depth iterations + for depth in range(max_depth): + print_color(f"\n===== Beam Search Depth {depth+1}/{max_depth} with {len(beams)} beams =====", 'blue') + + # Sample a validation minibatch for this depth + validation_xs, validation_infos = self._sample_minibatch( + validate_dataset, + validation_dataset_size + ) + + # Create a validation mini-dataset for this depth + validation_mini_dataset = { + 'inputs': validation_xs, + 'infos': validation_infos + } + + print_color(f"Sampled validation minibatch of size {len(validation_xs)} for depth {depth+1}", 'cyan') + + # Collect all expanded candidates + all_candidates = [] + + # Process each beam in the current set + for beam_idx, beam_params in enumerate(beams): + print_color(f"Processing beam {beam_idx+1}/{len(beams)}", 'yellow') + + # Expand: Generate multiple proposals from this beam (without evaluation) + beam_candidates = self.expand( + beam_params=beam_params, + beam_idx=beam_idx, + guide=guide, + train_dataset=train_dataset, + batch_size=batch_size, + num_proposals=num_proposals, + num_threads=num_threads + ) + + # Add all candidates to the pool for selection + all_candidates.extend(beam_candidates) + self.total_samples += batch_size + # Select: Evaluate all candidates and choose the top beam_width + beams, scores = self.select( + candidates=all_candidates, + validate_guide=validate_guide, + validation_mini_dataset=validation_mini_dataset, + beam_width=beam_width, + num_threads=num_threads, + min_score=min_score, + return_scores=True # Modified to return scores as well + ) + self.total_samples += validation_dataset_size*len(all_candidates) + # Track validation scores for this depth + if len(scores) > 0: + best_score = max(scores) + best_idx = scores.index(best_score) + best_params = beams[best_idx] + metrics['best_validation_scores'].append(best_score) + metrics['depth_scores'].append(scores) + + print_color(f"Depth {depth+1} - Best validation score: {best_score:.4f}", 'green') + + # Evaluate on test set every test_frequency steps + if test_dataset is not None and ((depth + 1) % test_frequency == 0): + # Update agent with best parameters from this depth + self.optimizer.update(best_params) + # Print best parameters + print_color("\nBest parameters at depth {}:".format(depth + 1), 'cyan') + for key, value in best_params.items(): + # Try to get a clean string name from the key, which might be a parameter object + if hasattr(key, 'name'): + # Extract string name from parameter object + param_name = key.name + else: + # If it's already a string or doesn't have a name attribute, use it directly + param_name = str(key) + print_color(f"{param_name}: {value}", 'cyan') + print_color("", 'cyan') # Empty line for readability + # Evaluate on test set + test_scores = evaluate( + self.agent, + guide, + test_dataset['inputs'], + test_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description=f"Evaluating best parameters at depth {depth+1} on test set" + ) + test_score = np.mean(test_scores) if all([s is not None for s in test_scores]) else -np.inf + + # Record the test score + metrics['test_scores'].append(test_score) + metrics['test_depths'].append(depth + 1) + + print_color(f"Depth {depth+1} - Test score: {test_score:.4f}", 'magenta') + + # Final selection - choose the best beam using FULL validation set + print_color("\n===== Final Selection Using Full Validation Set =====", 'blue') + + # Use select method with the full validation dataset + full_validation_dataset = { + 'inputs': validate_dataset['inputs'], + 'infos': validate_dataset['infos'] + } + + # Select the single best beam from the final candidates + best_beams, final_val_scores = self.select( + candidates=beams, + validate_guide=validate_guide, + validation_mini_dataset=full_validation_dataset, + beam_width=1, # Only select the best one + num_threads=num_threads, + min_score=min_score, + return_scores=True # Return scores too + ) + + # Get the best parameters + best_params = best_beams[0] + final_validation_score = final_val_scores[0] if final_val_scores else -np.inf + + # Apply the best parameters + self.optimizer.update(best_params) + + # Print out the final proposal candidate parameters + print_color("\n===== Final Proposal Candidate Parameters =====", 'magenta') + for param in self.agent.parameters(): + # Use a try-except block to handle parameter lookup + try: + # Check if parameter object is directly available as a key + if param in best_params: + param_value = best_params[param] + # Try to find by name if available + elif hasattr(param, 'name') and param.name in best_params: + param_value = best_params[param.name] + else: + param_value = "Parameter not found in best_params" + + # Get the parameter name directly + param_name = param.name if hasattr(param, 'name') else str(param) + print_color(f"{param_name}: {param_value}", 'blue') + except Exception as e: + print_color(f"Error accessing parameter {getattr(param, 'name', str(param))}: {e}", 'red') + continue + + # Evaluate on test set for reporting (if provided) + if test_dataset is not None: + final_test_scores = evaluate( + self.agent, + guide, + test_dataset['inputs'], + test_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description="Evaluating best beam on test set" + ) + final_test_score = np.mean(final_test_scores) if all([s is not None for s in final_test_scores]) else -np.inf + else: + final_test_score = None + + if final_test_score is not None: + print_color(f"BEST BEAM - Test score: {final_test_score:.4f}", 'green') + + # Save the best model + if save_frequency is not None and save_frequency > 0: + self.save_agent(save_path, 0) + + # Print periodic test scores summary if available + if metrics['test_scores']: + print_color("\n===== Periodic Test Scores Summary =====", 'blue') + for depth, score in zip(metrics['test_depths'], metrics['test_scores']): + print_color(f"Depth {depth}: Test score = {score:.4f}", 'cyan') + + # For API consistency with other algorithms + return metrics, final_test_score if final_test_score is not None else 0.0 + + def _sample_minibatch(self, dataset, batch_size): + """Sample a minibatch from the dataset.""" + indices = np.random.choice(len(dataset['inputs']), min(batch_size, len(dataset['inputs'])), replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def expand(self, + beam_params: Dict, + beam_idx: int, + guide, + train_dataset, + batch_size: int, + num_proposals: int, + num_threads: int = None) -> List[Dict]: + """ + Expands a single candidate into multiple proposals without evaluation. + + Args: + beam_params: Parameters of the current beam + beam_idx: Index of the current beam + guide: Guide for generating feedback + train_dataset: Training dataset + batch_size: Training batch size + num_proposals: Number of proposals to generate + num_threads: Number of threads to use + + Returns: + List of parameter dictionaries for each candidate + """ + # Restore parameters for this beam + self.optimizer.update(beam_params) + + # Run forward pass on minibatch to get outputs and feedbacks + xs_batch, infos_batch = self._sample_minibatch(train_dataset, batch_size) + + # Forward the agent on the minibatch + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs = async_run([self.forward]*len(xs_batch), + [(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)], + max_workers=num_threads, + description=f"Forward pass (beam {beam_idx+1}, batch size: {len(xs_batch)})") + else: + outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)] + + # Prepare for optimizer backward and step + scores, targets, feedbacks = [], [], [] + for target, score, feedback in outputs: + scores.append(score) + targets.append(target) + feedbacks.append(feedback) + target = batchify(*targets) + feedback = batchify(*feedbacks).data + + # Backward pass to compute gradients + self.optimizer.zero_feedback() + self.optimizer.backward(target, feedback) + + # Generate multiple proposals + step_kwargs = dict(bypassing=True, verbose='output') + candidates = [] + + # Generate num_proposals candidates + if use_asyncio: + update_dicts = async_run([self.optimizer.step]*num_proposals, + kwargs_list=[step_kwargs] * num_proposals, + max_workers=num_threads, + description=f"Generating {num_proposals} proposals for beam {beam_idx+1}") + else: + update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(num_proposals)] + + # Collect all valid proposals + for update_dict in update_dicts: + if len(update_dict) > 0: + # Make sure update_dict contains all parameters from beam_params + # Add any missing parameters from beam_params to update_dict + for param_key, param_value in beam_params.items(): + if param_key not in update_dict: + update_dict[param_key] = param_value + candidates.append(update_dict) + + # Also include the original beam parameters as a candidate + candidates.append(beam_params) + + return candidates + + def select(self, + candidates: List[Dict], + validate_guide, + validation_mini_dataset, + beam_width: int, + num_threads: int = None, + min_score: float = None, + return_scores: bool = False) -> Union[List[Dict], Tuple[List[Dict], List[float]]]: + """ + Evaluates all candidates and selects the top beam_width candidates based on validation scores. + + Args: + candidates: List of parameter dictionaries for each candidate + validate_guide: Guide for validation + validation_mini_dataset: Validation dataset for evaluation + beam_width: Maximum number of candidates to select + num_threads: Number of threads to use + min_score: Minimum score when errors occur + return_scores: Whether to return scores along with parameters + + Returns: + If return_scores is False: List of selected candidates' parameters + If return_scores is True: Tuple of (list of parameters, list of scores) + """ + # Store current parameters to restore later + current_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + # List to store (score, params) pairs + scored_candidates = [] + + # Evaluate each candidate + for candidate_idx, candidate_params in enumerate(candidates): + self.optimizer.update(candidate_params) + + # Evaluate on validation minibatch using evaluate function + validation_scores = evaluate( + self.agent, + validate_guide, + validation_mini_dataset['inputs'], + validation_mini_dataset['infos'], + min_score=min_score, + num_threads=num_threads, + description=f"Validating candidate {candidate_idx+1}/{len(candidates)}" + ) + + validation_score = np.mean(validation_scores) if all([s is not None for s in validation_scores]) else -np.inf + scored_candidates.append((validation_score, candidate_params)) + + print_color(f"Candidate {candidate_idx+1}: Validation score: {validation_score:.4f}", 'cyan') + + # Restore original parameters + self.optimizer.update(current_params) + + # Extract scores for logging + scores = [score for score, _ in scored_candidates] + + # If the number of candidates is less than or equal to beam_width, keep all of them + if len(scored_candidates) <= beam_width: + print_color(f"Keeping all {len(scored_candidates)} candidates as num_candidates <= beam_width. Scores: {[f'{s:.4f}' for s in scores]}", 'green') + selected_params = [params for _, params in scored_candidates] + if return_scores: + return selected_params, scores + return selected_params + + # Sort candidates by score (descending) + sorted_candidates = sorted(scored_candidates, key=lambda x: x[0], reverse=True) + + # Select top beam_width candidates + selected_candidates = sorted_candidates[:beam_width] + selected_scores = [score for score, _ in selected_candidates] + selected_params = [params for _, params in selected_candidates] + + print_color(f"Selected top {beam_width} beams with scores: {[f'{s:.4f}' for s in selected_scores]}", 'green') + if return_scores: + return selected_params, selected_scores + return selected_params + + + +class BeamsearchHistoryAlgorithm(BeamsearchAlgorithm): + """ + BeamsearchHistoryAlgorithm enhances BeamsearchAlgorithm by incorporating + historical parameter-score information into the proposal generation process. + + It maintains a log of previously selected parameter sets and their validation scores. + This history is then formatted and provided as additional context (feedback) + during the `expand` phase, aiming to guide the optimizer towards generating + more informed proposals based on past performance. + """ + + def train(self, + guide, + train_dataset, + *, + validate_dataset=None, + validate_guide=None, + validation_dataset_size=5, + beam_width=3, + batch_size=1, + num_proposals=1, + max_depth=2, + num_threads=10, + max_history_size=10, # Max number of history entries to keep + test_frequency=5, # Match the context file value + # Add other args from parent if needed, or rely on **kwargs + **kwargs + ): + """ + Performs beam search enhanced with parameter history. + + Args: + max_history_size: Maximum number of (parameter, score) pairs to store + in the history log. Defaults to 20. + top_k: Size of the top-k candidates buffer that persists across depths. + Default is 1, which keeps only the best candidate. + Other args are the same as BeamsearchAlgorithm.train() + """ + self.total_samples = 0 + self.min_score = kwargs.get('min_score', 0) + print_color(f"Running BeamsearchHistoryAlgorithm with beam_width={beam_width}, max_depth={max_depth}, max_history_size={max_history_size}", 'blue') + + # Initialize history log + self.parameter_history: List[Tuple[Dict, float]] = [] + self.max_history_size = max_history_size + + # Use train dataset for validation if not specified + validate_dataset = validate_dataset or train_dataset + validate_guide = validate_guide or guide + + + # Default validation dataset size + if validation_dataset_size is None: + validation_dataset_size = min(10, len(validate_dataset['inputs'])) + print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') + + # Store original parameters + original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + # Dictionary to track metrics + metrics = { + 'best_validation_scores': [], + 'depth_scores': [], + 'test_scores': [], + 'test_depths': [] + } + + test_dataset = kwargs.get('test_dataset', None) + + # Evaluate initial parameters on test set + if test_dataset is not None: + print_color("\n===== Evaluating Initial Parameters =====", 'blue') + initial_test_scores = evaluate( + self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description="Evaluating initial parameters on test set" + ) + initial_test_score = np.mean(initial_test_scores) if all([s is not None for s in initial_test_scores]) else -np.inf + print_color(f"Initial test score: {initial_test_score:.4f}", 'yellow') + metrics['test_scores'].append(initial_test_score) + metrics['test_depths'].append(1) # Start depth at 1 for consistency + + # Start with a single beam + beams = [original_params] + + # >>> Main Beam Search Loop <<< + for depth in range(max_depth): + print_color(f"\n===== Beam Search Depth {depth+1}/{max_depth} with {len(beams)} beams =====", 'blue') + + # Sample validation minibatch + validation_xs, validation_infos = self._sample_minibatch(validate_dataset, validation_dataset_size) + validation_mini_dataset = {'inputs': validation_xs, 'infos': validation_infos} + print_color(f"Sampled validation minibatch of size {len(validation_xs)} for depth {depth+1}", 'cyan') + + # Expand all current beams + all_candidates = [] + for beam_idx, beam_params in enumerate(beams): + print_color(f"Processing beam {beam_idx+1}/{len(beams)}", 'yellow') + beam_candidates = self.expand( # Calls the overridden expand method + beam_params=beam_params, beam_idx=beam_idx, guide=guide, + train_dataset=train_dataset, batch_size=batch_size, + num_proposals=num_proposals, num_threads=num_threads + ) + all_candidates.extend(beam_candidates) + self.total_samples += batch_size + # Select top candidates + beams, scores = self.select( + candidates=all_candidates, validate_guide=validate_guide, + validation_mini_dataset=validation_mini_dataset, beam_width=beam_width, + num_threads=num_threads, min_score=self.min_score, return_scores=True + ) + self.total_samples += validation_dataset_size*len(all_candidates) + # --- Populate History Log --- + if scores: + best_score_this_depth = -np.inf + for params, score in zip(beams, scores): + # params = copy.deepcopy(params) + # for name, value in params.items(): + # print(f"{name}: {value}") + if score > -np.inf: # Only log valid scores + # Store deep copies to prevent modification + self.parameter_history.append((params, score)) + best_score_this_depth = max(best_score_this_depth, score) + + # Keep history log bounded + if len(self.parameter_history) > self.max_history_size: + # Keep the ones with most recent + self.parameter_history = self.parameter_history[-self.max_history_size:] + # --- History Log Populated --- + + # Track metrics + if best_score_this_depth > -np.inf: + metrics['best_validation_scores'].append(best_score_this_depth) + metrics['depth_scores'].append(scores) + print_color(f"Depth {depth+1} - Best validation score: {best_score_this_depth:.4f}", 'green') + + best_idx = scores.index(best_score_this_depth) # Find index of best score + best_params = beams[best_idx] # Get corresponding params + + # Evaluate on test set periodically + if test_dataset is not None and ((depth + 1) % test_frequency == 0): + self.optimizer.update(best_params) # Use best params from this depth + print_color("\nBest parameters at depth {}:".format(depth + 1), 'cyan') + + for param in self.agent.parameters(): + # Use a try-except block to handle parameter lookup + try: + # Check if parameter object is directly available as a key + if param in best_params: + param_value = best_params[param] + # Try to find by name if available + elif hasattr(param, 'name') and param.name in best_params: + param_value = best_params[param.name] + else: + param_value = "Parameter not found in best_params" + + # Get the parameter name directly + param_name = param.name if hasattr(param, 'name') else str(param) + print_color(f"{param_name}: {param_value}", 'blue') + except Exception as e: + print_color(f"Error accessing parameter {getattr(param, 'name', str(param))}: {e}", 'red') + continue + test_scores_eval = evaluate( + self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description=f"Evaluating best parameters at depth {depth+1} on test set" + ) + test_score = np.mean(test_scores_eval) if all([s is not None for s in test_scores_eval]) else -np.inf + metrics['test_scores'].append(test_score) + metrics['test_depths'].append(depth + 1) + print_color(f"Depth {depth+1} - Test score: {test_score:.4f}", 'magenta') + + # >>> End Main Loop <<< + + # Final selection using full validation set + print_color("\n===== Final Selection Using Full Validation Set =====", 'blue') + full_validation_dataset = {'inputs': validate_dataset['inputs'], 'infos': validate_dataset['infos']} + best_beams, final_val_scores = self.select( + candidates=beams, validate_guide=validate_guide, + validation_mini_dataset=full_validation_dataset, beam_width=1, # Select only the best + num_threads=num_threads, min_score=self.min_score, return_scores=True + ) + + final_validation_score = final_val_scores[0] if final_val_scores else -np.inf + best_params = best_beams[0] if best_beams else original_params # Fallback to original if empty + + # Apply best parameters + self.optimizer.update(best_params) + + # Print final parameters + print_color("\n===== Final Proposal Candidate Parameters =====", 'magenta') + + # Final evaluation on test set + final_test_score = None + if test_dataset is not None: + final_test_scores_eval = evaluate( + self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description="Evaluating best beam on test set" + ) + final_test_score = np.mean(final_test_scores_eval) if all([s is not None for s in final_test_scores_eval]) else -np.inf + print_color(f"BEST BEAM - Test score: {final_test_score:.4f}", 'green') + + # Save agent if configured + if kwargs.get('save_frequency', None) is not None and kwargs['save_frequency'] > 0: + self.save_agent(kwargs.get('save_path', "checkpoints/agent.pkl"), 0) + + # Print test score summary + if metrics['test_scores']: + print_color("\n===== Periodic Test Scores Summary =====", 'blue') + for d, s in zip(metrics['test_depths'], metrics['test_scores']): + print_color(f"Depth {d}: Test score = {s:.4f}", 'cyan') + + return metrics, final_test_score if final_test_score is not None else -np.inf + + def expand(self, + beam_params: Dict, + beam_idx: int, + guide, + train_dataset, + batch_size: int, + num_proposals: int, + num_threads: int = None) -> List[Dict]: + """ + Expands a single candidate into multiple proposals, incorporating history. + + Overrides the parent expand method to augment the feedback provided to the + optimizer with a summary of historical parameter-score pairs. + + Args: Same as parent expand method. + + Returns: Same as parent expand method. + """ + # Restore parameters for this beam + self.optimizer.update(beam_params) + + # Run forward pass on minibatch to get outputs and feedbacks + xs_batch, infos_batch = self._sample_minibatch(train_dataset, batch_size) + + use_asyncio = self._use_asyncio(num_threads) + description=f"Forward pass (beam {beam_idx+1}, batch size: {len(xs_batch)})" + if use_asyncio: + outputs = async_run([self.forward]*len(xs_batch), + [(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)], + max_workers=num_threads, description=description) + else: + outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)] + + # Prepare original feedback + scores, targets, feedbacks = [], [], [] + for target, score, feedback_item in outputs: + scores.append(score) + targets.append(target) + feedbacks.append(feedback_item) + target = batchify(*targets) + original_feedback = batchify(*feedbacks).data # Assuming .data gives the relevant part + + # --- History Injection --- + history_prompt = "\n--- History Context ---\n" + history_prompt += "Consider the following previously selected parameter sets and their validation scores when generating proposals:\n" + if not self.parameter_history: + history_prompt += "(No history available yet)\n" + else: + # Format history (e.g., last N entries) + # Sorting by score might be useful: sorted_history = sorted(self.parameter_history, key=lambda item: item[1], reverse=True) + display_history = self.parameter_history # Or sorted_history[:self.max_history_size] + for i, (hist_params, hist_score) in enumerate(display_history): + # Format parameters nicely + param_parts = [] + for k, v in hist_params.items(): + key_name = getattr(k, 'name', str(k)) # Get name attr if Parameter object + if isinstance(v, (float, np.floating)): + param_parts.append(f"{key_name}: {v:.4f}") + elif isinstance(v, (np.ndarray, list)) and len(v) > 5: # Truncate long lists/arrays + param_parts.append(f"{key_name}: [{', '.join(map(str, v[:2]))}...{str(v[-1])}]") + else: + param_parts.append(f"{key_name}: {v}") + param_str = ", ".join(param_parts) + history_prompt += f" Attempt {i+1} (Score: {hist_score:.4f}): {{{param_str}}}\n" + + # Combine history with original feedback + # This assumes the optimizer can handle string feedback or a dict. + # Adjust based on how your specific optimizer/trace uses feedback. + augmented_feedback: Union[str, Dict] + if isinstance(original_feedback, str): + augmented_feedback = f"--- Current Feedback ---\n{original_feedback}\n{history_prompt}" + elif isinstance(original_feedback, dict): + # Add history as a separate key, preserving original structure + augmented_feedback = original_feedback.copy() + augmented_feedback['history_context'] = history_prompt + # Ensure original feedback text (if any) is still prominent + if 'feedback' in augmented_feedback: + augmented_feedback['feedback'] = f"{augmented_feedback['feedback']}\n{history_prompt}" + elif 'prompt' in augmented_feedback: # Adapt if feedback is under 'prompt' key + augmented_feedback['prompt'] = f"{augmented_feedback['prompt']}\n{history_prompt}" + else: # Fallback if structure unknown + augmented_feedback['raw_feedback'] = original_feedback + + else: + # Attempt to stringify other types, may need refinement + try: + augmented_feedback = f"--- Current Feedback ---\n{str(original_feedback)}\n{history_prompt}" + print_color(f"Warning: Combined non-string/dict feedback with history prompt.", "yellow") + except Exception as e: + print_color(f"Error combining feedback with history: {e}. Using original feedback.", "red") + augmented_feedback = original_feedback # Fallback + + # --- End History Injection --- + + + # Backward pass using the augmented feedback + self.optimizer.zero_feedback() + self.optimizer.backward(target, augmented_feedback) # Pass augmented feedback here + + # Generate multiple proposals using optimizer.step + step_kwargs = dict(bypassing=True, verbose='output') + candidates = [] + description_step=f"Generating {num_proposals} proposals for beam {beam_idx+1} (with history)" + if use_asyncio: + update_dicts = async_run([self.optimizer.step]*num_proposals, + kwargs_list=[step_kwargs] * num_proposals, + max_workers=num_threads, + description=description_step) + else: + update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(num_proposals)] + + # Collect all valid proposals + for update_dict in update_dicts: + if len(update_dict) > 0: + # Make sure update_dict contains all parameters from beam_params + # Add any missing parameters from beam_params to update_dict + for param_key, param_value in beam_params.items(): + if param_key not in update_dict: + update_dict[param_key] = param_value + candidates.append(update_dict) + + # Also include the original beam parameters as a candidate + candidates.append(beam_params) + + return candidates + From ce5424629b0c441f596b6aab31c4bb61c3209b95 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 3 Jun 2025 18:32:24 -0500 Subject: [PATCH 002/172] update parameters --- examples/example_usage_trainer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/example_usage_trainer.py b/examples/example_usage_trainer.py index 78ff2793..dc33e76a 100644 --- a/examples/example_usage_trainer.py +++ b/examples/example_usage_trainer.py @@ -194,7 +194,7 @@ def main(): help='Number of training samples') parser.add_argument('--num_validate_samples', type=int, default=20, help='Number of validation samples') - parser.add_argument('--num_test_samples', type=int, default=1, + parser.add_argument('--num_test_samples', type=int, default=20, help='Number of test samples') # Model parameters @@ -210,7 +210,7 @@ def main(): help='Number of training epochs') parser.add_argument('--batch_size', type=int, default=2, help='Training batch size') - parser.add_argument('--num_threads', type=int, default=20, + parser.add_argument('--num_threads', type=int, default=50, help='Number of threads for parallel processing') parser.add_argument('--eval_frequency', type=int, default=2, help='How often to run evaluation') @@ -220,11 +220,11 @@ def main(): help='Random seed for reproducibility') # Algorithm-specific parameters - parser.add_argument('--beam_width', type=int, default=2, + parser.add_argument('--beam_width', type=int, default=3, help='Beam width for beam search algorithms') parser.add_argument('--num_proposals', type=int, default=2, help='Number of proposals for beam search algorithms') - parser.add_argument('--max_depth', type=int, default=5, + parser.add_argument('--max_depth', type=int, default=20, help='Maximum depth for beam search algorithms') parser.add_argument('--validation_dataset_size', type=int, default=20, help='Size of validation dataset for beam search') From 139c2db4782f498db1b5afbb9c09b83a238c5a9d Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 3 Jun 2025 20:12:10 -0500 Subject: [PATCH 003/172] upload three versions of UCB search algorithms with a buffer --- examples/example_usage_trainer.py | 77 ++- opto/trainer/algorithms/UCBsearch.py | 999 +++++++++++++++++++++++++++ 2 files changed, 1071 insertions(+), 5 deletions(-) create mode 100644 opto/trainer/algorithms/UCBsearch.py diff --git a/examples/example_usage_trainer.py b/examples/example_usage_trainer.py index dc33e76a..7f4f2f9d 100644 --- a/examples/example_usage_trainer.py +++ b/examples/example_usage_trainer.py @@ -15,12 +15,13 @@ from opto.trace.modules import Module from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm +from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm, HybridUCB_LLM, UCBSearchFunctionApproximationAlgorithm from opto.trainer.guide import AutoGuide from opto.trainer.utils import DefaultLogger from opto.utils.llm import LLM, LiteLLM # Set default model -os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" +# os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" @trace.model class Learner(Module): @@ -183,8 +184,8 @@ def main(): parser = argparse.ArgumentParser(description='Train agent using various algorithms') # Algorithm parameters - parser.add_argument('--algorithm_type', type=str, default='beamsearchhistory', - choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory'], + parser.add_argument('--algorithm_type', type=str, default='UCBSearchFunctionApproximationAlgorithm', + choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory', 'UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm'], help='Type of algorithm to use') # Dataset parameters @@ -197,7 +198,7 @@ def main(): parser.add_argument('--num_test_samples', type=int, default=20, help='Number of test samples') - # Model parameters + # LLM Model parameters parser.add_argument('--trace_model', type=str, default='vertex_ai/gemini-2.0-flash', help='Model to use for trace operations') parser.add_argument('--student_model', type=str, default='vertex_ai/gemini-2.0-flash', @@ -210,7 +211,7 @@ def main(): help='Number of training epochs') parser.add_argument('--batch_size', type=int, default=2, help='Training batch size') - parser.add_argument('--num_threads', type=int, default=50, + parser.add_argument('--num_threads', type=int, default=10, help='Number of threads for parallel processing') parser.add_argument('--eval_frequency', type=int, default=2, help='How often to run evaluation') @@ -233,6 +234,20 @@ def main(): parser.add_argument('--num_basicsearch_proposals', type=int, default=2, help='Number of proposals for basic search algorithm') + # UCB algorithm-specific parameters + parser.add_argument('--max_buffer_size', type=int, default=10, + help='Maximum buffer size for UCB algorithms') + parser.add_argument('--ucb_exploration_factor', type=float, default=1.0, + help='UCB exploration factor') + parser.add_argument('--alpha', type=float, default=0.3, + help='Alpha parameter for HybridUCB_LLM (probability of UCB vs LLM path)') + parser.add_argument('--num_search_iterations', type=int, default=100, + help='Number of search iterations for UCB algorithms') + parser.add_argument('--train_batch_size_ucb', type=int, default=2, + help='Training batch size for UCB algorithms') + parser.add_argument('--evaluation_batch_size', type=int, default=20, + help='Evaluation batch size for UCB algorithms') + args = parser.parse_args() # Set environment variables @@ -306,6 +321,36 @@ def main(): logger=logger, num_threads=args.num_threads ) + elif args.algorithm_type == 'UCBsearch': + algorithm = UCBSearchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads, + max_buffer_size=args.max_buffer_size, + ucb_exploration_factor=args.ucb_exploration_factor + ) + elif args.algorithm_type == 'HybridUCB_LLM': + algorithm = HybridUCB_LLM( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads, + max_buffer_size=args.max_buffer_size, + ucb_exploration_factor=args.ucb_exploration_factor, + alpha=args.alpha, + llm_model=args.trace_model + ) + elif args.algorithm_type == 'UCBSearchFunctionApproximationAlgorithm': + algorithm = UCBSearchFunctionApproximationAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger, + num_threads=args.num_threads, + max_buffer_size=args.max_buffer_size, + ucb_exploration_factor=args.ucb_exploration_factor, + llm_model=args.trace_model + ) else: raise ValueError(f"Unknown algorithm type: {args.algorithm_type}") @@ -338,6 +383,13 @@ def main(): elif args.algorithm_type == 'basicsearch': train_params["num_proposals"] = args.num_basicsearch_proposals + elif args.algorithm_type in ['UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm']: + train_params.update({ + "num_search_iterations": args.num_search_iterations, + "train_batch_size": args.train_batch_size_ucb, + "evaluation_batch_size": args.evaluation_batch_size + }) + # Start training print(f"Training with {args.algorithm_type} algorithm...") start_time = time.time() @@ -351,6 +403,21 @@ def main(): for depth, score in enumerate(metrics['best_validation_scores']): print(f" Depth {depth+1}: {score:.4f}") + elif args.algorithm_type in ['UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm']: + print("\nUCB Algorithm Metrics:") + if 'best_candidate_scores' in metrics and metrics['best_candidate_scores']: + print(f" Best candidate scores over iterations: {len(metrics['best_candidate_scores'])} recorded") + print(f" Final best candidate score: {metrics['best_candidate_scores'][-1]:.4f}") + if 'buffer_avg_score' in metrics and metrics['buffer_avg_score']: + print(f" Final buffer average score: {metrics['buffer_avg_score'][-1]:.4f}") + if args.algorithm_type == 'HybridUCB_LLM': + if 'llm_generation_failures' in metrics: + print(f" LLM generation failures: {metrics['llm_generation_failures']}") + if 'generation_path' in metrics: + ucb_count = metrics['generation_path'].count('ucb') + llm_count = metrics['generation_path'].count('llm') + print(f" Generation methods used - UCB: {ucb_count}, LLM: {llm_count}") + print(f"Final score: {final_score:.4f}") return metrics, final_score diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py new file mode 100644 index 00000000..3e08aef6 --- /dev/null +++ b/opto/trainer/algorithms/UCBsearch.py @@ -0,0 +1,999 @@ +import numpy as np +import copy +import time +import math +import json # For LLM output parsing +import re # For smart quote replacement +from collections import deque +from typing import Union, List, Tuple, Dict, Any, Optional +import random # Added for alpha probability + +from opto import trace +from opto.trainer.utils import async_run # Assuming print_color is in utils +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful +from opto.utils.llm import LiteLLM # For the selector LLM + +from opto.trace.nodes import ParameterNode +import warnings +from black import format_str, FileMode + + +def smart_quote_replacement(text: str) -> str: + """ + Intelligently replace single quotes with double quotes for JSON parsing. + Handles the specific case where we have mixed quotes like: + {'key': "value with 'nested' quotes"} + """ + # For the specific pattern we're seeing, let's handle it step by step: + + # Step 1: Replace single quotes around keys + # Pattern: 'key': -> "key": + text = re.sub(r"'([^']*?)'(\s*:)", r'"\1"\2', text) + + # Step 2: For values that start with double quotes and contain single quotes, + # we need to escape the internal single quotes or convert them properly + + # Let's try a more direct approach for the problematic case: + # Find patterns like: "text with 'word' more text" + # We need to escape the internal single quotes + def escape_internal_quotes(match): + content = match.group(1) + # Replace single quotes inside with escaped single quotes + # Actually, for JSON we can leave single quotes as-is inside double quotes + return f'"{content}"' + + # Replace the pattern: : "content with 'quotes'" -> : "content with 'quotes'" + # (This should already be valid JSON) + + # The main issue is with the outer structure, let's fix that: + # If the string starts/ends with single quotes around the whole thing + text = text.strip() + if text.startswith("{'") and text.endswith("'}"): + # Replace the outer single quotes but preserve the content + # This is the pattern: {'str0': "content", 'str1': "more content"} + text = '{"' + text[2:-2] + '"}' + + return text + + +class UCBSearchAlgorithm(MinibatchAlgorithm): + """ + UCB Search Algorithm. + + Keeps a buffer of candidates with their statistics (score sum, evaluation count). + In each iteration: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a''. + 4. Evaluates 'a'' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a'' (with its validation stats) to the buffer. + 7. If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + self.ucb_exploration_factor = ucb_exploration_factor + + # To ensure optimizer_step can be called with bypassing=True if needed. + # This depends on the specific optimizer's implementation. + # For now, we assume the optimizer has a step method that can return parameters. + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 # Tracks total number of individual candidate evaluations used in UCB calculation for log(T) + self._candidate_id_counter = 0 + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], # Changed from validate_dataset + guide, # Changed from validate_guide + evaluation_batch_size: int, # New parameter name + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset (now typically train_dataset).""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + self.optimizer.update(params_to_eval_dict) + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, # Use main guide + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') # Explore unvisited states first + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def train(self, + guide, # Guide for train_dataset (feedback generation AND evaluation) + train_dataset: Dict[str, List[Any]], + *, + num_search_iterations: int = 100, + train_batch_size: int = 2, + evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_agent.pkl", + min_score_for_agent_update: Optional[float] = None, # Renamed from min_score to avoid conflict with evaluate's min_score + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + **kwargs + ) -> Tuple[Dict[str, Any], float]: # Returns metrics and best score + """ + Main training loop for UCB Search Algorithm. + """ + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate + total_samples = 0 + + # Metrics tracking + metrics = { + 'best_candidate_scores': [], # Score of the best candidate (e.g., highest mean) found so far at each iteration + 'selected_action_ucb': [], # UCB score of the selected action 'a' + 'new_candidate_scores': [], # Score of the new candidate 'a_prime' + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + } + +# 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. + print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, train_dataset, guide, evaluation_batch_size, num_threads # Use train_dataset and guide + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later + 'eval_count': initial_evals, + 'ucb_score': 0.0, # Will be updated + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() # Update UCB for the initial candidate + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + # 1. Pick the candidate 'a' with the highest UCB from the buffer + self._update_buffer_ucb_scores() # Ensure UCB scores are fresh + action_candidate_a = self.select(self.buffer) + + + print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') + + + # 2. Load parameters of 'a' into the agent for the optimizer update step + self.optimizer.update(action_candidate_a['params']) + + # 3. Draw minibatch from the training set, do update from 'a' to get 'a_prime' + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration}: Training minibatch empty, skipping optimizer step.", 'yellow') + continue + + # Perform forward pass and get feedback for agent parameters 'a' + outputs_for_a = [] + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration}: Forward pass for action 'a' ") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: # Should not happen if train_xs was not empty + print_color(f"Iter {iteration}: No outputs from forward pass for candidate 'a'. Skipping.", 'yellow') + continue + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) # Grads for 'a' are now in optimizer + + try: + a_prime_params_dict = self.optimizer.step(bypassing=True, verbose='output') + if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: + print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') + # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + except Exception as e: + print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') + continue + + # 4. Evaluate 'a_prime' on samples of validation set + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads # Use train_dataset and guide + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += evaluation_batch_size + train_batch_size + metrics['new_candidate_scores'].append(a_prime_score) + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + + # 5. Update the stats of 'a' (action_candidate_a) based on the training batch experience + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) # score is often an average + action_candidate_a['eval_count'] += len(train_xs) # or 1 if score is total + self._total_evaluations_tracker += len(train_xs) # training batch also counts as evaluations for UCB total T + + # 6. Add 'a_prime' (with its validation stats) to the buffer + if a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, # Store sum + 'eval_count': a_prime_evals, + 'ucb_score': 0.0, # Will be updated + 'iteration_created': iteration + } + + # Eviction logic before adding if buffer is at max_len + if len(self.buffer) == self.max_buffer_size: + self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + print_color(f"Iter {iteration}: Buffer full. Evicted a candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate to buffer.", 'magenta') + else: + print_color(f"Iter {iteration}: New candidate a_prime had invalid score/evals, not added to buffer.", 'yellow') + + # Update all UCB scores in the buffer after potential additions/removals/stat updates + self._update_buffer_ucb_scores() + + # Logging + best_in_buffer = max(self.buffer, key=lambda c: c['score_sum']/(c['eval_count'] or 1)) + metrics['best_candidate_scores'].append(best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1)) + metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], #best_candidate_score_in_buffer + "selected_action_ucb": action_candidate_a['ucb_score'], + "new_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_tracker": self._total_evaluations_tracker, + "total_samples": total_samples # Add new metric + } + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') + + # Save agent (e.g., the one with highest mean score in buffer) + if save_frequency is not None and iteration % save_frequency == 0: + best_overall_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9) ) + self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer + self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') + + # End of search loop + print_color("UCB search finished.", 'blue') + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + return metrics, -np.inf + + # Select the best candidate based on highest mean score (exploitation) + final_best_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') + + # Load best parameters into the agent + self.optimizer.update(final_best_candidate['params']) # Load params using optimizer + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Could be subclassed to implement different selection strategies''' + return max(buffer, key=lambda c: c['ucb_score']) + + +class HybridUCB_LLM(MinibatchAlgorithm): + """ + UCB Search Algorithm with Function Approximation (LLM). + + Keeps a buffer of candidates. + In each iteration: + - With probability alpha: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a_prime'. + 4. Evaluates 'a_prime' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a_prime' (with its validation stats) to the buffer. + - With probability 1-alpha: + 1. Uses an external LLM, prompted with candidates from the buffer, to generate a new candidate 'a_prime'. + 2. Evaluates 'a_prime' on a validation set minibatch. + 3. Adds 'a_prime' (with its validation stats) to the buffer. + If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, + alpha: float = 0.7, + llm_model: str = "vertex_ai/gemini-2.0-flash", + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.alpha = alpha + self.llm_model = llm_model + self.llm_prompt_budget_factor = 0.5 + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + self.ucb_exploration_factor = ucb_exploration_factor + + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 + + # Initialize LiteLLM + self.llm = LiteLLM(model=self.llm_model) + print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], + guide, + evaluation_batch_size: int, + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params_backup = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + try: + self.optimizer.update(params_to_eval_dict) + except Exception as e: + print_color(f"Error updating agent with params_to_eval_dict: {e}. Using current agent state for eval.", "red") + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params_backup) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params_backup) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + if total_tracked_evaluations == 0: + total_tracked_evaluations = 1 + + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations + 1e-9) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: + """ + Prompts an LLM with current buffer candidates to generate new string values for parameters. + Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. + """ + print_color("Attempting to generate candidate using LLM...", "blue") + if not self.buffer: + print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") + return None + + sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) + prompt_candidates = sorted_buffer + + serializable_candidate_summaries = [] + for cand_entry in prompt_candidates: + summary = { + "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, + "eval_count": cand_entry['eval_count'], + "ucb_score": round(cand_entry.get('ucb_score',0), 4), + } + serializable_candidate_summaries.append(summary) + + example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} + + prompt_messages = [ + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + ] + + print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") + + llm_response = self.llm(prompt_messages) + llm_response_str = llm_response.choices[0].message.content + + if not llm_response_str: + print_color("LLM returned an empty response.", "red") + return None + + # Clean the response string + cleaned_llm_response_str = llm_response_str.strip() + if cleaned_llm_response_str.startswith("```json"): + cleaned_llm_response_str = cleaned_llm_response_str[7:] + if cleaned_llm_response_str.endswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[:-3] + elif cleaned_llm_response_str.startswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[3:] + if cleaned_llm_response_str.endswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[:-3] + cleaned_llm_response_str = cleaned_llm_response_str.strip() + + if not cleaned_llm_response_str: + print_color("LLM response was empty after cleaning markdown/whitespace.", "red") + return None + + print_color(f"Cleaned LLM response: '{cleaned_llm_response_str}'", "magenta") + + # Fix common JSON formatting issues from LLM responses + try: + llm_params_raw = json.loads(cleaned_llm_response_str) + except json.JSONDecodeError as e: + print_color(f"Initial JSON parsing failed: {e}", "yellow") + print_color("Attempting to fix JSON formatting...", "yellow") + + fixed_json_str = smart_quote_replacement(cleaned_llm_response_str) + + try: + llm_params_raw = json.loads(fixed_json_str) + print_color("Successfully fixed JSON formatting", "green") + except json.JSONDecodeError as e2: + print_color(f"Smart quote replacement failed: {e2}", "yellow") + try: + simple_fixed = cleaned_llm_response_str.replace("'", '"') + llm_params_raw = json.loads(simple_fixed) + print_color("Fallback simple replacement succeeded", "green") + except json.JSONDecodeError as e3: + print_color(f"All JSON parsing attempts failed: {e3}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + if not isinstance(llm_params_raw, dict): + print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + candidate_params_dict = self.construct_update_dict(llm_params_raw) + return candidate_params_dict + + def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: + """Convert the suggestion in text into the right data type.""" + update_dict = {} + for node in self.agent.parameters(): + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + if type(formatted_suggestion) == str and 'def' in formatted_suggestion: + formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError) as e: + if getattr(self, 'ignore_extraction_error', False): + warnings.warn( + f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" + ) + else: + raise e + return update_dict + + def train(self, + guide, + train_dataset: Dict[str, List[Any]], + *, + num_search_iterations: int = 100, + train_batch_size: int = 5, + evaluation_batch_size: int = 5, + ensure_improvement: bool = False, + improvement_threshold: float = 0., + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_llm_agent.pkl", + min_score_for_agent_update: Optional[float] = None, + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + **kwargs + ) -> Tuple[Dict[str, Any], float]: + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update + total_samples = 0 + + metrics = { + 'best_candidate_scores': [], + 'selected_action_ucb': [], + 'new_candidate_scores': [], + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + 'llm_generation_failures': 0, + 'generation_path': [] + } + + # Initial candidate evaluation + print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, train_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, + 'eval_count': initial_evals, + 'ucb_score': 0.0, + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + self._update_buffer_ucb_scores() + a_prime_params_dict = None + a_prime_score = -np.inf + a_prime_evals = 0 + generation_method = "none" + + if random.random() < self.alpha: # UCB Path + generation_method = "ucb" + metrics['generation_path'].append("ucb") + if not self.buffer: + print_color(f"Iter {iteration} (UCB Path): Buffer empty, cannot select action. Skipping.", "red") + continue + + action_candidate_a = self.select(self.buffer) + + selected_mean_score = action_candidate_a['score_sum'] / action_candidate_a['eval_count'] if action_candidate_a['eval_count'] > 0 else -np.inf + print_color(f"Iter {iteration} (UCB Path): Selected action candidate (UCB: {action_candidate_a['ucb_score']:.4f}, MeanScore: {selected_mean_score:.4f} Evals: {action_candidate_a['eval_count']})", 'blue') + metrics['selected_action_ucb'].append(action_candidate_a['ucb_score']) + + self.optimizer.update(action_candidate_a['params']) + + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration} (UCB Path): Training minibatch empty, skipping optimizer step.", 'yellow') + continue + + total_samples += len(train_xs) + + # Forward pass for 'a' + outputs_for_a = [] + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration} (UCB): Forward for 'a'") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: + print_color(f"Iter {iteration} (UCB Path): No outputs from forward pass for 'a'. Skipping.", 'yellow') + continue + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) + + # Get a_prime by optimizer step + try: + returned_params = self.optimizer.step(bypassing=True, verbose=(verbose if isinstance(verbose, str) else 'output')) + if not isinstance(returned_params, dict) or not returned_params: + print_color(f"Iter {iteration} (UCB Path): Optimizer.step did not return a valid param dict for a_prime. Using current agent params.", 'yellow') + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + else: + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in returned_params} + + except Exception as e: + print_color(f"Iter {iteration} (UCB Path): Error during optimizer.step for a_prime: {e}. Skipping.", 'red') + continue + + # Evaluate a_prime (from UCB path) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += a_prime_evals + + # Update stats of action_candidate_a + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) + action_candidate_a['eval_count'] += len(train_xs) + self._total_evaluations_tracker += len(train_xs) + + print_color(f"Iter {iteration} (UCB Path): New candidate a_prime (from UCB) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + + else: # LLM Path + generation_method = "llm" + metrics['generation_path'].append("llm") + print_color(f"Iter {iteration} (LLM Path): Generating candidate via LLM.", 'blue') + a_prime_params_dict = self._llm_generate_candidate() + + if a_prime_params_dict: + # Evaluate a_prime (from LLM path) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += a_prime_evals + print_color(f"Iter {iteration} (LLM Path): New candidate a_prime (from LLM) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + else: + print_color(f"Iter {iteration} (LLM Path): LLM failed to generate a valid candidate. Skipping addition to buffer.", 'red') + metrics['llm_generation_failures'] += 1 + continue + + # Common logic for adding a_prime to buffer + metrics['new_candidate_scores'].append(a_prime_score) + + if a_prime_params_dict and a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, + 'eval_count': a_prime_evals, + 'ucb_score': 0.0, + 'iteration_created': iteration + } + + if len(self.buffer) == self.max_buffer_size: + self._update_buffer_ucb_scores() + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + evicted_mean_score = candidate_to_evict['score_sum'] / candidate_to_evict['eval_count'] if candidate_to_evict['eval_count'] > 0 else -np.inf + print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f}, MeanScore: {evicted_mean_score:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate (from {generation_method}) to buffer.", 'magenta') + elif a_prime_params_dict: + print_color(f"Iter {iteration}: New candidate a_prime (from {generation_method}) had invalid score/evals ({a_prime_score}, {a_prime_evals}), not added to buffer.", 'yellow') + + self._update_buffer_ucb_scores() + + # Logging + if self.buffer: + best_in_buffer = max(self.buffer, key=lambda c: (c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1))) + current_best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] if best_in_buffer['eval_count'] > 0 else 1) + metrics['best_candidate_scores'].append(current_best_score) + + valid_scores = [c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1) for c in self.buffer if c['eval_count'] > 0] + metrics['buffer_avg_score'].append(np.mean(valid_scores) if valid_scores else -np.inf) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + else: + metrics['best_candidate_scores'].append(-np.inf) + metrics['buffer_avg_score'].append(-np.inf) + metrics['buffer_avg_evals'].append(0) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], + "newly_evaluated_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_ucb_T": self._total_evaluations_tracker, + "total_samples": total_samples, + "generation_method_this_iter": generation_method, + "llm_generation_total_failures": metrics['llm_generation_failures'] + } + if generation_method == "ucb" and metrics['selected_action_ucb']: + log_data["selected_action_ucb"] = metrics['selected_action_ucb'][-1] + + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Gen method: {generation_method}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') + + if save_frequency is not None and iteration % save_frequency == 0 and self.buffer: + best_overall_candidate_entry = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) + self.optimizer.update(best_overall_candidate_entry['params']) + if hasattr(self, 'save_agent'): + self.save_agent(save_path, iteration) + best_mean_score_for_save = best_overall_candidate_entry['score_sum'] / (best_overall_candidate_entry['eval_count'] if best_overall_candidate_entry['eval_count'] > 0 else 1E-9) + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer (Mean Score: {best_mean_score_for_save:.4f}).", 'green') + else: + print_color(f"Iter {iteration}: save_agent method not found, skipping save.", 'yellow') + + print_color("UCB-LLM search finished.", 'blue') + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + return metrics, -np.inf + + final_best_candidate = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] if final_best_candidate['eval_count'] > 0 else 1E-9) + final_best_evals = final_best_candidate['eval_count'] + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_evals}", 'green') + + self.optimizer.update(final_best_candidate['params']) + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Selects candidate with highest UCB score.''' + if not buffer: return None + return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) + + +class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): + """ + UCB Search Algorithm that uses LLM function approximation to select candidates. + """ + + def __init__(self, llm_model, *args, **kwargs): + super().__init__(*args, **kwargs) + self.llm_model = llm_model + self.llm = LiteLLM(model=self.llm_model) + print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") + + def select(self, buffer): + """Generate a new candidate entry using LLM. Note: this doesn't add it to the buffer.""" + new_action_params = self._llm_generate_candidate() + new_candidate_entry = { + 'params': new_action_params, + 'score_sum': 0, + 'eval_count': 0, + 'ucb_score': 0.0, + 'iteration_created': 0 + } + return new_candidate_entry + + def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: + """ + Prompts an LLM with current buffer candidates to generate new string values for parameters. + Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. + """ + print_color("Attempting to generate candidate using LLM...", "blue") + if not self.buffer: + print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") + return None + + sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) + prompt_candidates = sorted_buffer + + serializable_candidate_summaries = [] + for cand_entry in prompt_candidates: + summary = { + "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, + "eval_count": cand_entry['eval_count'], + "ucb_score": round(cand_entry.get('ucb_score',0), 4), + } + serializable_candidate_summaries.append(summary) + + example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} + + prompt_messages = [ + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + ] + + print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") + + llm_response = self.llm(prompt_messages) + llm_response_str = llm_response.choices[0].message.content + + if not llm_response_str: + print_color("LLM returned an empty response.", "red") + return None + + # Clean the response string + cleaned_llm_response_str = llm_response_str.strip() + if cleaned_llm_response_str.startswith("```json"): + cleaned_llm_response_str = cleaned_llm_response_str[7:] + if cleaned_llm_response_str.endswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[:-3] + elif cleaned_llm_response_str.startswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[3:] + if cleaned_llm_response_str.endswith("```"): + cleaned_llm_response_str = cleaned_llm_response_str[:-3] + cleaned_llm_response_str = cleaned_llm_response_str.strip() + + if not cleaned_llm_response_str: + print_color("LLM response was empty after cleaning markdown/whitespace.", "red") + return None + + print_color(f"Cleaned LLM response: '{cleaned_llm_response_str}'", "magenta") + + # Fix common JSON formatting issues from LLM responses + try: + llm_params_raw = json.loads(cleaned_llm_response_str) + except json.JSONDecodeError as e: + print_color(f"Initial JSON parsing failed: {e}", "yellow") + print_color("Attempting to fix JSON formatting...", "yellow") + + fixed_json_str = smart_quote_replacement(cleaned_llm_response_str) + + try: + llm_params_raw = json.loads(fixed_json_str) + print_color("Successfully fixed JSON formatting", "green") + except json.JSONDecodeError as e2: + print_color(f"Smart quote replacement failed: {e2}", "yellow") + try: + simple_fixed = cleaned_llm_response_str.replace("'", '"') + llm_params_raw = json.loads(simple_fixed) + print_color("Fallback simple replacement succeeded", "green") + except json.JSONDecodeError as e3: + print_color(f"All JSON parsing attempts failed: {e3}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + if not isinstance(llm_params_raw, dict): + print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + candidate_params_dict = self.construct_update_dict(llm_params_raw) + return candidate_params_dict + + def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: + """Convert the suggestion in text into the right data type.""" + update_dict = {} + for node in self.agent.parameters(): + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + if type(formatted_suggestion) == str and 'def' in formatted_suggestion: + formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError) as e: + if getattr(self, 'ignore_extraction_error', False): + warnings.warn( + f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" + ) + else: + raise e + return update_dict + From d75bea4551dc79abba8a805ec386aea8f4bfa02e Mon Sep 17 00:00:00 2001 From: adith387 Date: Thu, 5 Jun 2025 16:45:05 -0700 Subject: [PATCH 004/172] Update OVERVIEW.md --- OVERVIEW.md | 81 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/OVERVIEW.md b/OVERVIEW.md index 947180c6..e53a8c19 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -1,32 +1,57 @@ # Overview of Trace and Development Guide -The library of Trace is designed to be a lightweight, modularized package to allow developers to easily try new ideas on generative optimization and integrate learning wtih their pipelines. - -Currently, the Trace library has three main modules collected under the `opto` top module. - -1. `opto.trace` provides the infrastructure for tracing computational workflows. It defines two primitives `trace.node` and `@trace.bundle`. They can be applied to Python objects and methods, respectively, which define the root nodes and operators of the directed acyclic graph (DAG) of computation. They both have a `trainable` flag. When set `True`, the wrapped objects are viewed as *parameters* of the computational worflow. Users can use `trace.node` and `@trace.bundle` to declare the data and computation that they wish to trace and/or adapt, and we call the resulting workflow defined by these two primitives a *traced* workflow. When running a traced workflow, a DAG will be automatiically created by Trace as a data structure, which will later be sent to optimizers in `opto.optimizers`for updates (upon calling `node.backward` with soem feedback). - -2. `opto.optimizers` has a collection of generative optimization algorithms, whose API is defined by an abstract class `Optimizer`. Think them like gradient algorithms. Their job is to propose a new version of the parameters (i.e. those set with `trainable=True`) when receiving a computational graph (DAG) and the feedback given to the computed output. Typically, these algorithms can be viewed as an LLM agent, which makes calls to LLM to analyze the computational graph and the feedback, and to propose updates. In Trace library, we provide implementation of several popular optimizers, such `OptoPrime`, `TextGrad`, and `OPRO`. - -3. `opto.trainers` are a collection of training algorithms (under the `AlgorithmBase` class) that use optimizers in `opto.optimizers` as subroutines to improve a given workflow following a feedback oracle constructed by datasets, interactive environments, etc. While `Optimizer` defines a low-level *optimization* API, `AlgorithmBase` defines a high-level *learning* API which standarizes the format of agent (by the `Module` class created by `@trace.model`), the data loader (by the `DataLoader` class), and the feedback oracle (by the `AutoGuide` class). With this common abstraction, we offer training algorithms, from the basic `MinibatchAlgorithm` which trains minibatches of samples to search algorithms like `BeamSearch`. The `AlgorithmBase` also handles logging of the training process. While there are overlapping between the functions of `Optimizer` and `AlgorithmBase`, the main distinction is that algorithms under `AlgorithmBase` are meta algorithms, as they should work for different optimizers in `opto.optimizers`. - - -4. `opto.utils` has a collection of helper functions and backends, which are reusable for various applications. This includes, e.g., abstraction of LLMs, database, etc. Making use of all these utils would requie installing optional depedencies. - - -In summary, `opto.trace` is the infrastructure, `opto.optimizers` are algorithms that process feedback and propose new parameter candidates, and `opto.trainers` are algorithms built on top of `opto.trace` and `opto.optimizers` to train learning agents. - -## Common Workflow of Using Trace - -1. Use `trace.node` and `@trace.bundle` to define the traceable workflow and its trainable parameter. -2. Wrap the workflow as a `trace.Module` using `@trace.model` -3. Create a dataloader using `DataLoader` and define the feedback oracle (an analogy of loss function) using `AutoGuide`. -4. Create a trainer from `opto.trainers` using optimizers from `opto.optimizers` and the above module, dataloader, and feedback oracle. +The Trace library is a lightweight, modular package designed to allow developers to experiment easily with generative optimization and integrate feedback-driven learning into their computational workflows. +The library has four modules within the `opto` top-level namespace: + +1. `opto.trace` provides the infrastructure for converting executing Python code into symbolic directed acyclic graphs (DAGs). +It defines two tracing primitives: + - `trace.node`: Wraps Python objects, designating them as nodes within the computational graph. + - `@trace.bundle`: Decorates Python methods/functions, marking them as operators within the graph. + +Each primitive has a `trainable` flag. +When set to `True`, these marked nodes and bundles become the trainable *parameters* of the workflow. +By using these primitives, developers can create a *traced workflow* represented as a DAG. +This DAG structure is automatically constructed at runtime, capturing both computational dependencies and trainable parameters, ready for optimization. + +2. `opto.optimizers` has an abstract class `Optimizer` that defines algorithms that take computation DAGs and associated feedback objects as input, and output values for the trainable parameters. +These algorithms are analogous to gradient-based optimizers in PyTorch, but are typically implemented as generative optimization agents, leveraging LLMs to analyze feedback and propose parameter updates. +We provide implementations of several generative optimizers: + - `OptoPrime` + - `TextGrad` + - `OPRO` + +3. `opto.trainers` has the `AlgorithmBase` abstraction that orchestrates the overall training process. +Trainers manage data handling, tracing control, feedback collection, optimizer invocation, and iterating/stopping. Specifically, a trainer: + - Controls data sampling (via `DataLoader`). + - Determines when DAGs are constructed and when feedback (e.g. via `AutoGuide`) is collected . + - Invokes `optimizers` for parameter updates, possibly repeatedly and manages the training loop. + - Logs training progress. + +Although `optimizers` handle lower-level optimization decisions, trainers under `AlgorithmBase` manage broader training logic and are designed to be compatible across various `optimizers`. +We provide implementations of common trainers: `MinibatchAlgorithm`(basic minibatch training) and `BeamSearch` (example of search-based training). + +4. `opto.utils` has a collection of reusable helper functions and backend utilities, including abstraction for: + - Large Language Models (LLMs) + - Databases + - Miscellaneous support tools. + +Note: Some utilities might require installing optional depedencies. + +## Concise Summary of Abstractions + - `trace`: Infrastructure to construct symbolic computational DAGs + - `optimizers`: Receive DAG and feedback, output parameter values. + - `trainer`: Manages DAG construction, data sampling, feedback collection, optimizer invocation, and training workflow control. + +## Common Workflow for Using Trace + +1. Define a traceable workflow with `trace.node` and `@trace.bundle`, marking trainable parameters. +2. Wrap this workflow into a `trace.Module` with `@trace.model`. +3. Define a dataloader (`DataLoader`) and feedback oracle (analogous to a loss function, using e.g. `AutoGuide`). +4. Instantiate a trainer from `opto.trainers`, specifying the optimizer from `opto.optimizers` alongside the defined module above, dataloader, and feedback oracle. 5. Run the trainer. - -## Common Workflow of Improving Trace -- **Developing new optimization agent** Contribute to `trace.optimizers` and design new algorithms under `Optimizer` -- **Developing new learning algorithms** Contribute to `trace.trainers` (and `trace.optimizers` when necessary). Design new algorithms under `AlgorithmBase`, new dataloader under `DataLoader`, or new feedback oracle under `AutoGuide`. -- **Improving infrastructure** Propose updates to change `opto.trace` (e.g., to improve UI, add new tracing, etc.) -- **Onboarding other utility tools** Add to `opto.utils` and update `setup.py` with optional requirements. \ No newline at end of file +## Guidelines for Improving and Extending Trace + - **New optimization agents**: Contribute to `opto.optimizers`, sub-class from the `Optimizer` abstraction. + - **New learning algorithms**: Contribute to `opto.trainers` (and optionally `opto.optimizers` if necessary). Design new algorithms sub-classing `AlgorithmBase`, new dataloader under `DataLoader`, or new feedback oracle under `AutoGuide`. + - **Improving infrastructure**: Propose modifications to `opto.trace` to improve tracing capability, user experience, or additional functionality. + - **Onboarding other utility tools**: Add helpful tools to `opto.utils` and update `setup.py` accordingly for optional dependencies. From c8b158cd80cbff48504e979a71272096d00ee5a3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 6 Jun 2025 23:05:59 +0000 Subject: [PATCH 005/172] RenameOptoprimeBatchOpt to OptoPrimeV2 --- opto/optimizers/__init__.py | 4 ++-- opto/optimizers/{optoprime_batchopt.py => optoprime_v2.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename opto/optimizers/{optoprime_batchopt.py => optoprime_v2.py} (99%) diff --git a/opto/optimizers/__init__.py b/opto/optimizers/__init__.py index e03b7f93..e40f9c36 100644 --- a/opto/optimizers/__init__.py +++ b/opto/optimizers/__init__.py @@ -2,6 +2,6 @@ from opto.optimizers.optoprimemulti import OptoPrimeMulti from opto.optimizers.opro import OPRO from opto.optimizers.textgrad import TextGrad -from opto.optimizers.optoprime_batchopt import OptoprimeBatchOpt +from opto.optimizers.optoprime_v2 import OptoPrimeV2 -__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoprimeBatchOpt"] \ No newline at end of file +__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2"] \ No newline at end of file diff --git a/opto/optimizers/optoprime_batchopt.py b/opto/optimizers/optoprime_v2.py similarity index 99% rename from opto/optimizers/optoprime_batchopt.py rename to opto/optimizers/optoprime_v2.py index c34265dd..f0c78258 100644 --- a/opto/optimizers/optoprime_batchopt.py +++ b/opto/optimizers/optoprime_v2.py @@ -3,7 +3,7 @@ from opto.optimizers.optoprime import OptoPrime -class OptoprimeBatchOpt(OptoPrime): +class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( """ From 4058bb1770cbd3917d015beeb2ce70b6a5e51289 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 21:30:26 +0000 Subject: [PATCH 006/172] Add projection api. --- opto/optimizers/optimizer.py | 9 +++++++++ opto/optimizers/optoprime.py | 4 ---- opto/trace/bundle.py | 6 ++++++ opto/trace/nodes.py | 10 ++++++++++ opto/trace/projections/__init__.py | 1 + opto/trace/projections/projections.py | 28 +++++++++++++++++++++++++++ tests/unit_tests/test_projection.py | 16 +++++++++++++++ 7 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 opto/trace/projections/__init__.py create mode 100644 opto/trace/projections/projections.py create mode 100644 tests/unit_tests/test_projection.py diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index ea2a0503..77ee10db 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -54,10 +54,19 @@ def trace_graph(self): def step(self, bypassing=False, *args, **kwargs): update_dict = self.propose(*args, **kwargs) + self.project(update_dict) if not bypassing: self.update(update_dict) return update_dict # TODO add reasoning + def project(self, update_dict: Dict[ParameterNode, Any]): + """Project the update dictionary onto the feasible set.""" + for p, d in update_dict.items(): + if p.trainable: + for projection in p.projections: + d = projection.project(d) + update_dict[p] = d + def propose(self, *args, **kwargs): """Propose the new data of the parameters based on the feedback.""" return self._step(*args, **kwargs) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6ac4ce95..faa52df6 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -478,11 +478,7 @@ def construct_update_dict( for node in self.parameters: if node.trainable and node.py_name in suggestion: try: - from black import format_str, FileMode formatted_suggestion = suggestion[node.py_name] - # use black formatter for code reformatting - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) update_dict[node] = type(node.data)(formatted_suggestion) except (ValueError, KeyError) as e: # catch error due to suggestion missing the key or wrong data type diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index ce080360..570c0833 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -39,6 +39,7 @@ def bundle( catch_execution_error=True, allow_external_dependencies=False, overwrite_python_recursion=False, + projections=None, ): """Wrap a function as a FunModule which returns node objects. @@ -53,6 +54,7 @@ def bundle( catch_execution_error (bool, optional): Whether to catch exceptions during operator execution. Defaults to True. allow_external_dependencies (bool, optional): Whether to allow external dependencies. Defaults to False. overwrite_python_recursion (bool, optional): Whether to overwrite Python recursion behavior. Defaults to False. + projections (List[Projection], optional): List of projections to be used in updating trainable parameter. Defaults to None. Returns: FunModule: The wrapped function that returns node objects. @@ -70,6 +72,7 @@ def decorator(fun): allow_external_dependencies=allow_external_dependencies, overwrite_python_recursion=overwrite_python_recursion, _ldict=prev_f_locals, # Get the locals of the calling function + projections=None, ) return fun_module @@ -124,6 +127,7 @@ def __init__( catch_execution_error=True, allow_external_dependencies=False, overwrite_python_recursion=False, + projections=None, _ldict=None, ): @@ -183,10 +187,12 @@ def __init__( signature = re.search(r"\s*(def.*:)", source).group(1) else: signature = signature_sr.group(1) + self.parameter = ParameterNode( self.info["source"], name="__code", constraint="The code should start with:\n" + signature, + projections=projections, ) @property diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index a05e662c..ebfd4153 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2007,6 +2007,7 @@ def __init__( trainable=True, description="[ParameterNode] This is a ParameterNode in a computational graph.", constraint=None, + projections=None, # a list of Projection info=None, ) -> None: if description is None or description == "": @@ -2027,6 +2028,15 @@ def __init__( info=info, ) self._dependencies["parameter"].add(self) + if projections is not None: + assert isinstance( + projections, list + ), "Projections must be a list of Projection objects." + from opto.trace.projection import Projection + assert all( + isinstance(p, Projection) for p in projections + ), "All projections must be instances of Projection." + self._projections = projections def __str__(self) -> str: # str(node) allows us to look up in the feedback dictionary easily diff --git a/opto/trace/projections/__init__.py b/opto/trace/projections/__init__.py new file mode 100644 index 00000000..f029f4f1 --- /dev/null +++ b/opto/trace/projections/__init__.py @@ -0,0 +1 @@ +from opto.trace.projections.projections import Projection, BlackCodeFormatter \ No newline at end of file diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py new file mode 100644 index 00000000..262202e2 --- /dev/null +++ b/opto/trace/projections/projections.py @@ -0,0 +1,28 @@ +from opto.trace.nodes import ParameterNode + + +class Projection: + """ + Abstract base class for projection methods. + """ + + def __init__(self, *args, **kwargs): + pass + + def project(self, x: ParameterNode) -> ParameterNode: + """ + Project the parameter node `x` onto the feasible set. + """ + raise NotImplementedError("Subclasses should implement this method.") + + +class BlackCodeFormatter(Projection): + # This requires the `black` package to be installed. + + def project(self, x: str) -> str: + # importing here to avoid necessary dependencies on black + # use black formatter for code reformatting + from black import format_str, FileMode + if type(x) == str and 'def' in x: + x = format_str(x, mode=FileMode()) + return x diff --git a/tests/unit_tests/test_projection.py b/tests/unit_tests/test_projection.py new file mode 100644 index 00000000..c0ada6e9 --- /dev/null +++ b/tests/unit_tests/test_projection.py @@ -0,0 +1,16 @@ +from opto.trace.projections import BlackCodeFormatter + +def test_black_code_formatter(): + code = """ +def example_function(): + print("Hello, World!") + + + print("This is a test function.") + + + + """ + projection = BlackCodeFormatter() + formatted_code = projection.project(code) + assert formatted_code == 'def example_function():\n print("Hello, World!")\n\n print("This is a test function.")\n' From f9ce8f0e68cca68cdf4769e10823bf25c23b3332 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 21:58:44 +0000 Subject: [PATCH 007/172] Add docstring projection. --- opto/trace/projections/__init__.py | 3 ++- opto/trace/projections/code_projections.py | 31 ++++++++++++++++++++++ opto/trace/projections/projections.py | 13 +-------- tests/unit_tests/test_projection.py | 24 ++++++++++++++++- 4 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 opto/trace/projections/code_projections.py diff --git a/opto/trace/projections/__init__.py b/opto/trace/projections/__init__.py index f029f4f1..7264d5bd 100644 --- a/opto/trace/projections/__init__.py +++ b/opto/trace/projections/__init__.py @@ -1 +1,2 @@ -from opto.trace.projections.projections import Projection, BlackCodeFormatter \ No newline at end of file +from opto.trace.projections.projections import Projection +from opto.trace.projections.code_projections import BlackCodeFormatter, DocstringProjection \ No newline at end of file diff --git a/opto/trace/projections/code_projections.py b/opto/trace/projections/code_projections.py new file mode 100644 index 00000000..78a4642c --- /dev/null +++ b/opto/trace/projections/code_projections.py @@ -0,0 +1,31 @@ + +from opto.trace.projections import Projection + +class BlackCodeFormatter(Projection): + # This requires the `black` package to be installed. + + def project(self, x: str) -> str: + # importing here to avoid necessary dependencies on black + # use black formatter for code reformatting + from black import format_str, FileMode + if type(x) == str and 'def' in x: + x = format_str(x, mode=FileMode()) + return x + +class DocstringProjection(Projection): + """ + Projection that formats docstrings. + """ + def __init__(self, docstring: str): + self.docstring = docstring + + def project(self, x: str) -> str: + """ Replace the docstring in the code wit the stored docstring. """ + if type(x) == str and '"""' in x: + # replace the docstring in the code with the stored docstring + x = x.split('"""', 2) + if len(x) > 2: + x = f'{x[0]}"""{self.docstring}"""{x[2]}' + else: + x = f'{x[0]}"""{self.docstring}"""' + return x \ No newline at end of file diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py index 262202e2..4c799f3a 100644 --- a/opto/trace/projections/projections.py +++ b/opto/trace/projections/projections.py @@ -14,15 +14,4 @@ def project(self, x: ParameterNode) -> ParameterNode: Project the parameter node `x` onto the feasible set. """ raise NotImplementedError("Subclasses should implement this method.") - - -class BlackCodeFormatter(Projection): - # This requires the `black` package to be installed. - - def project(self, x: str) -> str: - # importing here to avoid necessary dependencies on black - # use black formatter for code reformatting - from black import format_str, FileMode - if type(x) == str and 'def' in x: - x = format_str(x, mode=FileMode()) - return x + \ No newline at end of file diff --git a/tests/unit_tests/test_projection.py b/tests/unit_tests/test_projection.py index c0ada6e9..794fffcd 100644 --- a/tests/unit_tests/test_projection.py +++ b/tests/unit_tests/test_projection.py @@ -1,4 +1,4 @@ -from opto.trace.projections import BlackCodeFormatter +from opto.trace.projections import BlackCodeFormatter, DocstringProjection def test_black_code_formatter(): code = """ @@ -14,3 +14,25 @@ def example_function(): projection = BlackCodeFormatter() formatted_code = projection.project(code) assert formatted_code == 'def example_function():\n print("Hello, World!")\n\n print("This is a test function.")\n' + + +def test_docstring_projection(): + code = """ +def example_function(): + \"\"\"This is an example function.\"\"\" + print("Hello, World!") + """ + docstring = "This is a new docstring." + projection = DocstringProjection(docstring) + formatted_code = projection.project(code) + + new_code = """ +def example_function(): + \"\"\"This is a new docstring.\"\"\" + print("Hello, World!") + """ + + assert formatted_code == new_code + + # assert '"""This is a new docstring."""' in formatted_code + # assert 'print("Hello, World!")' in formatted_code \ No newline at end of file From 867c89c44a35480d56eac161650c73973c554106 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 22:02:10 +0000 Subject: [PATCH 008/172] Rename basic_algorithm.py to basic_algorithms.py --- examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py | 2 +- opto/trainer/algorithms/__init__.py | 2 +- opto/trainer/algorithms/aggregator.py | 2 +- .../algorithms/{basic_algorithm.py => basic_algorithms.py} | 0 tests/llm_optimizers_tests/test_trainer_refactored.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename opto/trainer/algorithms/{basic_algorithm.py => basic_algorithms.py} (100%) diff --git a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py index b0ed9b28..7e12339f 100644 --- a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py +++ b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py @@ -10,7 +10,7 @@ import autogen import pickle import os -from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate from opto.trainer.guide import AutoGuide diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index aac6a494..ea5dde63 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1 +1 @@ -from opto.trainer.algorithms.basic_algorithm import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm +from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm diff --git a/opto/trainer/algorithms/aggregator.py b/opto/trainer/algorithms/aggregator.py index 4f94d999..a1d30a67 100644 --- a/opto/trainer/algorithms/aggregator.py +++ b/opto/trainer/algorithms/aggregator.py @@ -9,7 +9,7 @@ from opto.trace.nodes import ParameterNode from opto.optimizers.utils import print_color from opto.trainer.algorithms import Minibatch -from opto.trainer.algorithms.basic_algorithm import standard_optimization_step +from opto.trainer.algorithms.basic_algorithms import standard_optimization_step from opto.utils.llm import LLM, AbstractModel diff --git a/opto/trainer/algorithms/basic_algorithm.py b/opto/trainer/algorithms/basic_algorithms.py similarity index 100% rename from opto/trainer/algorithms/basic_algorithm.py rename to opto/trainer/algorithms/basic_algorithms.py diff --git a/tests/llm_optimizers_tests/test_trainer_refactored.py b/tests/llm_optimizers_tests/test_trainer_refactored.py index 58b32dcd..74f1993b 100644 --- a/tests/llm_optimizers_tests/test_trainer_refactored.py +++ b/tests/llm_optimizers_tests/test_trainer_refactored.py @@ -4,7 +4,7 @@ from opto.utils.llm import LLM, LiteLLM from opto.optimizers.utils import print_color from opto.optimizers import OptoPrime -from opto.trainer.algorithms.basic_algorithm import BatchedFeedback +from opto.trainer.algorithms.basic_algorithms import BatchedFeedback from opto.trainer.guide import VerbalJudgeGuide from typing import Any From 00006a66158db002d35426ba06a8e89a59ba3dec Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 22:35:44 +0000 Subject: [PATCH 009/172] Fix bug in MinibatchAlgorithm due to an accidental commit --- opto/trainer/algorithms/basic_algorithms.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index d443367e..66596580 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -272,9 +272,6 @@ def update(self, outputs, *args, **kwargs): feedback = batchify(*feedbacks).data # str average_score = np.mean(scores) if all([s is not None for s in scores]) else None - fig = target.backward(visualize=True, retain_graph=True) - fig.render("minibatch.pdf") - # Update the agent using the feedback self.optimizer.zero_feedback() self.optimizer.backward(target, feedback) From eedb299c345cee6473c674b65411595adcaa1a43 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 9 Jun 2025 22:52:33 +0000 Subject: [PATCH 010/172] Rafactor loggers into loggers.py --- opto/trainer/algorithms/algorithm.py | 3 +- opto/trainer/loggers.py | 70 ++++++++++++++++++++++++++++ opto/trainer/utils.py | 27 ----------- setup.py | 3 +- 4 files changed, 74 insertions(+), 29 deletions(-) create mode 100644 opto/trainer/loggers.py diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 927d186f..9ec35fcc 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -1,7 +1,8 @@ import warnings from opto import trace from opto.trace.modules import Module -from opto.trainer.utils import async_run, DefaultLogger +from opto.trainer.utils import async_run +from opto.trainer.loggers import DefaultLogger import os diff --git a/opto/trainer/loggers.py b/opto/trainer/loggers.py new file mode 100644 index 00000000..cc14b657 --- /dev/null +++ b/opto/trainer/loggers.py @@ -0,0 +1,70 @@ + + +class BaseLogger: + + def log(self, name, data, step, **kwargs): + """Log a message with the given name and data at the specified step. + + Args: + name: Name of the metric + data: Value of the metric + step: Current step/iteration + **kwargs: Additional arguments (e.g., color) + """ + raise NotImplementedError("Subclasses should implement this method.") + + +class ConsoleLogger(BaseLogger): + """A simple logger that prints messages to the console.""" + + def log(self, name, data, step, **kwargs): + """Log a message to the console. + + Args: + name: Name of the metric + data: Value of the metric + step: Current step/iteration + **kwargs: Additional arguments (e.g., color) + """ + color = kwargs.get('color', None) + # Simple color formatting for terminal output + color_codes = { + 'green': '\033[92m', + 'red': '\033[91m', + 'blue': '\033[94m', + 'end': '\033[0m' + } + + start_color = color_codes.get(color, '') + end_color = color_codes['end'] if color in color_codes else '' + + print(f"[Step {step}] {start_color}{name}: {data}{end_color}") + + +class TensorboardLogger(BaseLogger): + """A logger that writes metrics to TensorBoard.""" + + def __init__(self, log_dir): + # Late import to avoid dependency issues + try: + from tensorboardX import SummaryWriter + except ImportError: + # try importing from torch.utils.tensorboard if tensorboardX is not available + from torch.utils.tensorboard import SummaryWriter + + self.writer = SummaryWriter(log_dir) + + def log(self, name, data, step, **kwargs): + """Log a message to TensorBoard. + + Args: + name: Name of the metric + data: Value of the metric + step: Current step/iteration + **kwargs: Additional arguments (not used here) + """ + self.writer.add_scalar(name, data, step) + +# TODO add wandb logger + +DefaultLogger = ConsoleLogger \ No newline at end of file diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index b8dad65c..717ff23b 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -47,33 +47,6 @@ async def _run(): return asyncio.run(_run()) -class DefaultLogger: - """A simple logger that prints messages to the console.""" - - def log(self, name, data, step, **kwargs): - """Log a message to the console. - - Args: - name: Name of the metric - data: Value of the metric - step: Current step/iteration - **kwargs: Additional arguments (e.g., color) - """ - color = kwargs.get('color', None) - # Simple color formatting for terminal output - color_codes = { - 'green': '\033[92m', - 'red': '\033[91m', - 'blue': '\033[94m', - 'end': '\033[0m' - } - - start_color = color_codes.get(color, '') - end_color = color_codes['end'] if color in color_codes else '' - - print(f"[Step {step}] {start_color}{name}: {data}{end_color}") - - if __name__ == "__main__": def tester(t): # regular time-consuming function diff --git a/setup.py b/setup.py index 5ab3a9a1..97c24f1b 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,9 @@ "litellm", "black", "scikit-learn", + "tensorboardX" ] - + setuptools.setup( name="trace-opt", version=__version__, From 4a2b799ebb9359ab5e29ffe3b9ac0c256d8423a7 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 17:16:31 +0000 Subject: [PATCH 011/172] Add an example of using trainer. Add a flag to disable using json_object format in OptoPrime. --- .../gsm8k_trainer_example.py | 55 ++++++----- opto/optimizers/__init__.py | 6 +- opto/optimizers/optoprime.py | 12 +-- tests/llm_optimizers_tests/test_trainer.py | 94 ------------------- 4 files changed, 42 insertions(+), 125 deletions(-) rename tests/llm_optimizers_tests/test_trainer_refactored.py => examples/gsm8k_trainer_example.py (54%) delete mode 100644 tests/llm_optimizers_tests/test_trainer.py diff --git a/tests/llm_optimizers_tests/test_trainer_refactored.py b/examples/gsm8k_trainer_example.py similarity index 54% rename from tests/llm_optimizers_tests/test_trainer_refactored.py rename to examples/gsm8k_trainer_example.py index 74f1993b..02ad6f65 100644 --- a/tests/llm_optimizers_tests/test_trainer_refactored.py +++ b/examples/gsm8k_trainer_example.py @@ -2,16 +2,16 @@ import numpy as np from opto import trace from opto.utils.llm import LLM, LiteLLM -from opto.optimizers.utils import print_color from opto.optimizers import OptoPrime -from opto.trainer.algorithms.basic_algorithms import BatchedFeedback +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm +from opto.trainer.loggers import DefaultLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @trace.model class Learner: - # A basic LLM agent. + """ A basic LLM agent. """ def __init__(self, system_prompt: str = "You're a helpful agent", user_prompt_template: str = "Query: {message}", @@ -22,9 +22,15 @@ def __init__(self, system_prompt: str = "You're a helpful agent", @trace.bundle() def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: - """ Call the LLM model. system_prompt specifies - the behavior of the agent. user prompt is the input to the agent, which - is formatted as user_prompt_template.format(message=message).""" + """Call the LLM model. + + Args: + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). + message: the input to the agent. It can be a query, a task, a code, etc. + Returns: + The response from the agent. + """ if '{message}' not in user_prompt_template: raise ValueError("user_prompt_template must contain '{message}'") @@ -39,9 +45,9 @@ def forward(self, message: Any) -> Any: """ Forward pass of the agent. """ return self.model(self.system_prompt, self.user_prompt_template, message) -class Logger: - def log(self, *messages, color=None, **kwargs): - print_color(messages, color=color) + +Guide = VerbalJudgeGuide +Logger = DefaultLogger def main(): @@ -49,32 +55,35 @@ def main(): seed = 42 num_epochs = 1 batch_size = 1 - eval_frequency = 1 - teacher_model = "gpt-4o-mini" #"gpt-4o-mini_2024-07-18" - student_model = "gpt-35-turbo_1106" + eval_frequency = -1 + teacher_model = None + student_model = None np.random.seed(seed) - train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][ - :10] # NOTE for now, we train on a smaller portion + # In this example, we use the GSM8K dataset, which is a dataset of math word problems. + # We will look the training error of the agent on a small portion of this dataset. + train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) - test_dataset = train_dataset # NOTE for now, we just look at training error - - agent = Learner(llm=LiteLLM(model="gpt-3.5-turbo")) - - guide = VerbalJudgeGuide(model=teacher_model) + test_dataset = train_dataset - alg = BatchedFeedback(agent=agent, - optimizer=OptoPrime(agent.parameters()), - logger=Logger()) + agent = Learner(llm=LLM(student_model)) + guide = Guide(model=teacher_model) + optimizer = OptoPrime(agent.parameters()) + alg = MinibatchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=Logger()) + alg.train(guide, train_dataset, num_epochs=num_epochs, batch_size=batch_size, eval_frequency=eval_frequency, test_dataset=test_dataset, - num_threads=3) + num_threads=3, + verbose=True,) if __name__ == "__main__": diff --git a/opto/optimizers/__init__.py b/opto/optimizers/__init__.py index e40f9c36..9b0b2007 100644 --- a/opto/optimizers/__init__.py +++ b/opto/optimizers/__init__.py @@ -1,7 +1,9 @@ -from opto.optimizers.optoprime import OptoPrime +from opto.optimizers.optoprime import OptoPrime as OptoPrimeV1 from opto.optimizers.optoprimemulti import OptoPrimeMulti from opto.optimizers.opro import OPRO from opto.optimizers.textgrad import TextGrad from opto.optimizers.optoprime_v2 import OptoPrimeV2 -__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2"] \ No newline at end of file +OptoPrime = OptoPrimeV1 + +__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2", "OptoPrimeV1"] \ No newline at end of file diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index faa52df6..5a5c5c36 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -259,6 +259,7 @@ def __init__( max_tokens=4096, log=True, prompt_symbols=None, + use_json_object_format=True, # whether to use json object format for the response when calling LLM **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) @@ -294,6 +295,7 @@ def __init__( self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) if prompt_symbols is not None: self.prompt_symbols.update(prompt_symbols) + self.use_json_object_format = use_json_object_format def default_propagator(self): """Return the default Propagator object of the optimizer.""" @@ -557,15 +559,13 @@ def call_llm( {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] - + + response_format = {"type": "json_object"} if self.use_json_object_format else None try: # Try tp force it to be a json object - response = self.llm( - messages=messages, - response_format={"type": "json_object"}, - max_tokens=max_tokens, - ) + response = self.llm(messages=messages, max_tokens=max_tokens, response_format=response_format) except Exception: response = self.llm(messages=messages, max_tokens=max_tokens) + response = response.choices[0].message.content if verbose: diff --git a/tests/llm_optimizers_tests/test_trainer.py b/tests/llm_optimizers_tests/test_trainer.py deleted file mode 100644 index 3f88cccb..00000000 --- a/tests/llm_optimizers_tests/test_trainer.py +++ /dev/null @@ -1,94 +0,0 @@ -import datasets -import numpy as np -from opto import trace -from opto.utils.llm import AutoGenLLM -from opto.optimizers.utils import print_color -from opto.optimizers import OptoPrime -from opto.trainer import train -from typing import Any - - -@trace.model -class Student: - # A basic LLM agent. - - def __init__(self, system_prompt: str = "You're a helpful agent", - user_prompt_template: str = "Query: {message}", - llm: AutoGenLLM = None): - self.system_prompt = trace.node(system_prompt, trainable=True) - self.user_prompt_template = trace.node(user_prompt_template) - self.llm = llm or AutoGenLLM() - - @trace.bundle() - def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: - """ Call the LLM model. system_prompt specifies - the behavior of the agent. user prompt is the input to the agent, which - is formatted as user_prompt_template.format(message=message).""" - - if '{message}' not in user_prompt_template: - raise ValueError("user_prompt_template must contain '{message}'") - - response = self.llm( - messages = [{"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt_template.format(message=message)}] - ) - return response.choices[0].message.content - - def forward(self, message: Any) -> Any: - """ Forward pass of the agent. """ - return self.model(self.system_prompt, self.user_prompt_template, message) - - -def teacher(student_answer, info, model="gpt-4o-mini_2024-07-18"): - """ Use LLM to evaluate the student answer. """ - llm = AutoGenLLM(filter_dict={"model": [model]}) - system_prompt = "You're a match teacher who helps students to learn. " - user_prompt_template = "The student answered: {}. The correct answer is {}. If the student answer is correct, please say 'Correct [TERMINATE]'. Otherwise, if the student answer is incorrect, please provide feedback to the student. The feedback should be specific and actionable." - true_answer = info - - response = llm( - messages = [{"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt_template.format(student_answer, true_answer)}] - ) - - response = response.choices[0].message.content - score = 1 if 'Correct [TERMINATE]' in response else 0 - return score, response - - - -class Logger: - def log(self, message, color=None, **kwargs): - print_color(message, color=color) - - - -def main(): - # set seed - seed = 42 - num_epochs = 1 - batch_size = 1 - eval_frequency = 1 - teacher_model = "gpt-4o-mini_2024-07-18" - student_model = "gpt-35-turbo_1106" - - np.random.seed(seed) - - train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] # NOTE for now, we train on a smaller portion - train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) - test_dataset = train_dataset # NOTE for now, we just look at training error - - - train(agent=Student(llm=AutoGenLLM(filter_dict={"model": ["gpt-35-turbo_1106"]})), - teacher=lambda *args, **kwargs : teacher(model=teacher_model, *args, **kwargs), - train_dataset=train_dataset, - num_epochs=num_epochs, - logger=Logger(), - batch_size=batch_size, - test_dataset=test_dataset, - eval_frequency=eval_frequency - ) - - -if __name__ == "__main__": - main() \ No newline at end of file From d32ce24003c27eaec6110522ce545962308afcf4 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 17:41:21 +0000 Subject: [PATCH 012/172] Finish implementation of Tensorboard logger. Add a flag to VerbalJudgeGuide to return LLM's response directly. --- examples/gsm8k_trainer_example.py | 15 +++++++++------ opto/trainer/guide.py | 25 ++++++++++++++++++------- opto/trainer/loggers.py | 22 ++++++++++++++++++---- setup.py | 3 ++- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index 02ad6f65..61c604f0 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -4,7 +4,7 @@ from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrime from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm -from opto.trainer.loggers import DefaultLogger +from opto.trainer.loggers import DefaultLogger, TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @@ -47,7 +47,7 @@ def forward(self, message: Any) -> Any: Guide = VerbalJudgeGuide -Logger = DefaultLogger +Logger = TensorboardLogger def main(): @@ -56,8 +56,9 @@ def main(): num_epochs = 1 batch_size = 1 eval_frequency = -1 - teacher_model = None - student_model = None + verbose = True + teacher_model = None # use default mode + student_model = None # use default mode np.random.seed(seed) @@ -70,11 +71,13 @@ def main(): agent = Learner(llm=LLM(student_model)) guide = Guide(model=teacher_model) optimizer = OptoPrime(agent.parameters()) + logger = Logger(verbose=verbose) + # set use_json_object_format=False if LLM does not support JSON object format alg = MinibatchAlgorithm( agent=agent, optimizer=optimizer, - logger=Logger()) + logger=logger) alg.train(guide, train_dataset, @@ -83,7 +86,7 @@ def main(): eval_frequency=eval_frequency, test_dataset=test_dataset, num_threads=3, - verbose=True,) + verbose='output' if verbose else False) if __name__ == "__main__": diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 5a11dcce..30c428a6 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -53,22 +53,26 @@ class VerbalJudgeGuide(AutoGuide): This is an implementation of LLM-as-a-judge. """ + DEFAULT_CORRECTNESS_TEMPLATE = "Correct [TERMINATE]" + DEFAULT_INCORRECTNESS_TEMPLATE = "Incorrect" + DEFAULT_PROMPT_TEMPLATE = ( - "The query is: {query}. The student answered: {response}. The correct answer is: {reference}. " - "If the student answer is correct, please say 'Correct [TERMINATE]'. " - "Otherwise, if the student answer is incorrect, please provide feedback to the student. " + "The query is: {query}.\n\n\nThe student answered: {response}.\n\n\nThe correct answer is: {reference}.\n\n\n" + "Reason whether the student answer is correct. If the student answer is correct, please say {correctness_template}. " + "Otherwise, if the student answer is incorrect, say {incorrectness_template} and provide feedback to the student. " "The feedback should be specific and actionable." ) DEFAULT_SYSTEM_PROMPT = "You're a helpful teacher who provides clear and constructive feedback." - DEFAULT_CORRECTNESS_TEMPLATE = "Correct [TERMINATE]" def __init__(self, model: Optional[str] = None, llm: Optional[AbstractModel] = None, prompt_template: Optional[str] = None, system_prompt: Optional[str] = None, - correctness_template: Optional[str] = None): + correctness_template: Optional[str] = None, + use_formatted_response: bool = True + ): """ Initialize the VerbalGuide with an LLM and prompt templates. @@ -78,12 +82,14 @@ def __init__(self, prompt_template: Custom prompt template with {response} and {reference} placeholders system_prompt: Custom system prompt for the LLM correctness_template: Template to use when response is deemed correct by metric + use_formatted_response: Whether to format the response with additional context; if False, the raw LLM response is returned """ self.model = model self.llm = llm or LLM(model=model) self.prompt_template = prompt_template or self.DEFAULT_PROMPT_TEMPLATE self.system_prompt = system_prompt or self.DEFAULT_SYSTEM_PROMPT self.correctness_template = correctness_template or self.DEFAULT_CORRECTNESS_TEMPLATE + self.use_formatted_response = use_formatted_response def get_feedback(self, query: str, response: str, reference: Optional[str] = None, **kwargs) -> Tuple[float, str]: """ @@ -103,7 +109,12 @@ def get_feedback(self, query: str, response: str, reference: Optional[str] = Non raise ValueError("ReferenceGuide requires reference information to generate feedback") # Check if metric function indicates perfect match - user_prompt = self.prompt_template.format(query=query, response=response, reference=reference) + user_prompt = self.prompt_template.format( + query=query, + response=response, + reference=reference, + correctness_template=self.DEFAULT_CORRECTNESS_TEMPLATE, + incorrectness_template=self.DEFAULT_INCORRECTNESS_TEMPLATE) messages = [ {"role": "system", "content": self.system_prompt}, @@ -128,7 +139,7 @@ def get_feedback(self, query: str, response: str, reference: Optional[str] = Non score = 1 if 'Correct [TERMINATE]' in llm_response else 0 - return score, formatted_response + return score, formatted_response if self.use_formatted_response else llm_response def forward(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]: score, feedback = self.get_feedback(task, response, info, **kwargs) diff --git a/opto/trainer/loggers.py b/opto/trainer/loggers.py index cc14b657..5f82a4ac 100644 --- a/opto/trainer/loggers.py +++ b/opto/trainer/loggers.py @@ -2,6 +2,11 @@ class BaseLogger: + def __init__(self, log_dir='./logs', **kwargs): + """Initialize the logger. This method can be overridden by subclasses.""" + self.log_dir = log_dir + pass + def log(self, name, data, step, **kwargs): """Log a message with the given name and data at the specified step. @@ -41,10 +46,12 @@ def log(self, name, data, step, **kwargs): print(f"[Step {step}] {start_color}{name}: {data}{end_color}") -class TensorboardLogger(BaseLogger): +class TensorboardLogger(ConsoleLogger): """A logger that writes metrics to TensorBoard.""" - def __init__(self, log_dir): + def __init__(self, log_dir='./logs', verbose=True, **kwargs): + super().__init__(log_dir, **kwargs) + self.verbose = verbose # Late import to avoid dependency issues try: from tensorboardX import SummaryWriter @@ -52,7 +59,7 @@ def __init__(self, log_dir): # try importing from torch.utils.tensorboard if tensorboardX is not available from torch.utils.tensorboard import SummaryWriter - self.writer = SummaryWriter(log_dir) + self.writer = SummaryWriter(self.log_dir) def log(self, name, data, step, **kwargs): """Log a message to TensorBoard. @@ -63,7 +70,14 @@ def log(self, name, data, step, **kwargs): step: Current step/iteration **kwargs: Additional arguments (not used here) """ - self.writer.add_scalar(name, data, step) + if self.verbose: + super().log(name, data, step, **kwargs) + if isinstance(data, str): + # If data is a string, log it as text + self.writer.add_text(name, data, step) + else: + # Otherwise, log it as a scalar + self.writer.add_scalar(name, data, step) # TODO add wandb logger diff --git a/setup.py b/setup.py index 97c24f1b..4fa7eef5 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ "litellm", "black", "scikit-learn", - "tensorboardX" + "tensorboardX", + "tensorboard" ] setuptools.setup( From c5e6322428944c17619a72416d9e24959dbd7cb5 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 18:40:32 +0000 Subject: [PATCH 013/172] Fix typos --- examples/gsm8k_trainer_example.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index 61c604f0..369eeec4 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -57,8 +57,9 @@ def main(): batch_size = 1 eval_frequency = -1 verbose = True - teacher_model = None # use default mode - student_model = None # use default mode + teacher_model = None # use default model + student_model = None # use default model + optimizer_model = None # use default model np.random.seed(seed) @@ -69,8 +70,8 @@ def main(): test_dataset = train_dataset agent = Learner(llm=LLM(student_model)) - guide = Guide(model=teacher_model) - optimizer = OptoPrime(agent.parameters()) + guide = Guide(model=LLM(teacher_model)) + optimizer = OptoPrime(agent.parameters(), llm=LiteLLM(optimizer_model)) logger = Logger(verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format From 728d8d232d99b5d6d2db08fbd2f82db19d770eff Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 19:29:49 +0000 Subject: [PATCH 014/172] Fix a bug that projections is private in ParameterNode. --- opto/trace/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index ebfd4153..31ec9da9 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2036,7 +2036,7 @@ def __init__( assert all( isinstance(p, Projection) for p in projections ), "All projections must be instances of Projection." - self._projections = projections + self.projections = projections def __str__(self) -> str: # str(node) allows us to look up in the feedback dictionary easily From 5ba9e4142c46c8dbb07d82a59420ee460a36f419 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 19:37:41 +0000 Subject: [PATCH 015/172] Fix the bug of missing self.projections in ParameterNode --- opto/trace/nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 31ec9da9..5764cb72 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2028,6 +2028,7 @@ def __init__( info=info, ) self._dependencies["parameter"].add(self) + if projections is not None: assert isinstance( projections, list @@ -2037,6 +2038,8 @@ def __init__( isinstance(p, Projection) for p in projections ), "All projections must be instances of Projection." self.projections = projections + else: + self.projections = [] def __str__(self) -> str: # str(node) allows us to look up in the feedback dictionary easily From c71aa5cf8527eb73d7179eb9a632a6a749c5e59b Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 13:13:00 -0700 Subject: [PATCH 016/172] add a __call__ method --- opto/trace/projections/projections.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py index 4c799f3a..f1a802c8 100644 --- a/opto/trace/projections/projections.py +++ b/opto/trace/projections/projections.py @@ -9,6 +9,18 @@ class Projection: def __init__(self, *args, **kwargs): pass + def __call__(self, x: ParameterNode) -> ParameterNode: + """ + Call the projection method on the parameter node `x`. + + Args: + x: The parameter node to project. + + Returns: + The projected parameter node. + """ + return self.project(x) + def project(self, x: ParameterNode) -> ParameterNode: """ Project the parameter node `x` onto the feasible set. From 909d2328246e1a8833f0c8b63e2f6905306e0f89 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 13:15:22 -0700 Subject: [PATCH 017/172] fix a nodes.py import issue (misspelling of package) --- opto/trace/__init__.py | 1 + opto/trace/nodes.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/opto/trace/__init__.py b/opto/trace/__init__.py index ddf2a778..ddf01300 100644 --- a/opto/trace/__init__.py +++ b/opto/trace/__init__.py @@ -4,6 +4,7 @@ from opto.trace.broadcast import apply_op import opto.trace.propagators as propagators import opto.trace.operators as operators +import opto.trace.projections as projections from opto.trace.nodes import Node, GRAPH from opto.trace.nodes import node diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index ebfd4153..c159624d 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -2032,7 +2032,7 @@ def __init__( assert isinstance( projections, list ), "Projections must be a list of Projection objects." - from opto.trace.projection import Projection + from opto.trace.projections import Projection assert all( isinstance(p, Projection) for p in projections ), "All projections must be instances of Projection." From df3cf5044bef60d7435e00642fa4de08fb1524a7 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 13:26:58 -0700 Subject: [PATCH 018/172] fix an error that projections were not passed into the ParameterNode in bundle --- opto/trace/bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index 570c0833..db51f8eb 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -72,7 +72,7 @@ def decorator(fun): allow_external_dependencies=allow_external_dependencies, overwrite_python_recursion=overwrite_python_recursion, _ldict=prev_f_locals, # Get the locals of the calling function - projections=None, + projections=projections, ) return fun_module From af5b339581838492439b7ab96d835163dd6bf0fb Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 14:36:34 -0700 Subject: [PATCH 019/172] initial commit --- opto/trace/modules.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index a85d1efb..89176864 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -1,6 +1,8 @@ import os import pickle import copy +import inspect +import textwrap from opto.trace.containers import ParameterContainer from opto.trace.nodes import ParameterNode @@ -11,7 +13,22 @@ def model(cls): """ class ModelWrapper(cls, Module): - pass + def model_dump(self, filename): + methods = [ + method for name, method in cls.__dict__.items() + if inspect.isfunction(method) + ] + + with open(filename, "w") as f: + f.write(f"class {cls.__name__}:\n") + + for i, method in enumerate(methods): + source = inspect.getsource(method) + source = textwrap.dedent(source) + indented = textwrap.indent(source, " ") + f.write(indented) + if i < len(methods) - 1: + f.write("\n") # only one newline between methods return ModelWrapper @@ -25,8 +42,8 @@ def forward(self, *args, **kwargs): def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) - def save(self, file_name): - """Save the parameters of the model to a file.""" + def save(self, file_name: str): + """Save the parameters of the model to a pickle file.""" # detect if the directory exists directory = os.path.dirname(file_name) if directory != "": @@ -35,7 +52,7 @@ def save(self, file_name): pickle.dump(copy.deepcopy(self.parameters_dict()), f) def load(self, file_name): - """Load the parameters of the model from a file.""" + """Load the parameters of the model from a pickle file.""" with open(file_name, "rb") as f: loaded_data = pickle.load(f) self._set(loaded_data) @@ -62,4 +79,4 @@ def _set(self, new_parameters): parameters_dict[k]._set(v) else: # if the parameter does not exist assert k not in self.__dict__ - setattr(self, k, v) + setattr(self, k, v) \ No newline at end of file From 0f47c6c49ae8b3dedb0db2c43985e269d0b8bbcc Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 23:48:36 +0000 Subject: [PATCH 020/172] Fix some import issues due to updates in experimental. --- examples/example_usage_trainer.py | 4 ++-- opto/trainer/algorithms/UCBsearch.py | 2 +- opto/trainer/algorithms/beamsearch_algorithm.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/example_usage_trainer.py b/examples/example_usage_trainer.py index 7f4f2f9d..32c6db70 100644 --- a/examples/example_usage_trainer.py +++ b/examples/example_usage_trainer.py @@ -13,11 +13,11 @@ from opto.optimizers import OptoPrime from opto.optimizers.utils import print_color from opto.trace.modules import Module -from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, BasicSearchAlgorithm +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm, HybridUCB_LLM, UCBSearchFunctionApproximationAlgorithm from opto.trainer.guide import AutoGuide -from opto.trainer.utils import DefaultLogger +from opto.trainer.loggers import DefaultLogger from opto.utils.llm import LLM, LiteLLM # Set default model diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 3e08aef6..d6460418 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -11,7 +11,7 @@ from opto import trace from opto.trainer.utils import async_run # Assuming print_color is in utils from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful from opto.utils.llm import LiteLLM # For the selector LLM from opto.trace.nodes import ParameterNode diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index 2d63f5a1..09a13578 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -3,7 +3,7 @@ from typing import Union, List, Tuple, Dict, Any, Optional from opto.trainer.utils import async_run from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithm import MinibatchAlgorithm, evaluate, batchify +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify class BeamsearchAlgorithm(MinibatchAlgorithm): From d2d69c3e26a23c9f9e6f34f31b4134ab0d00f2e3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 23:52:17 +0000 Subject: [PATCH 021/172] Rename file. --- examples/{example_usage_trainer.py => search_algo_example.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{example_usage_trainer.py => search_algo_example.py} (100%) diff --git a/examples/example_usage_trainer.py b/examples/search_algo_example.py similarity index 100% rename from examples/example_usage_trainer.py rename to examples/search_algo_example.py From 6b302c8fbe044a77f0091d228651ff2f64539dc5 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 10 Jun 2025 23:59:36 +0000 Subject: [PATCH 022/172] Fix a bug in CustomLLM's attributes not defined with model not being None. --- opto/utils/llm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index a84e4865..6f7f87c4 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -207,11 +207,11 @@ def __init__(self, model: Union[str, None] = None, reset_freq: Union[int, None] cache=True) -> None: if model is None: model = os.environ.get('TRACE_CUSTOMLLM_MODEL', 'gpt-4o') - base_url = os.environ.get('TRACE_CUSTOMLLM_URL', 'http://xx.xx.xxx.xx:4000/') - server_api_key = os.environ.get('TRACE_CUSTOMLLM_API_KEY', - 'sk-Xhg...') # we assume the server has an API key - # the server API is set through `master_key` in `config.yaml` for LiteLLM proxy server - + base_url = os.environ.get('TRACE_CUSTOMLLM_URL', 'http://xx.xx.xxx.xx:4000/') + server_api_key = os.environ.get('TRACE_CUSTOMLLM_API_KEY', + 'sk-Xhg...') # we assume the server has an API key + # the server API is set through `master_key` in `config.yaml` for LiteLLM proxy server + self.model_name = model self.cache = cache factory = lambda: self._factory(base_url, server_api_key) # an LLM instance uses a fixed model From cfe5ee156be00613927561151e46ac891dacec02 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 17:21:34 -0700 Subject: [PATCH 023/172] push the workable version (without unit test code yet) --- opto/trace/modules.py | 70 +++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 89176864..bdfbcda3 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -3,8 +3,9 @@ import copy import inspect import textwrap -from opto.trace.containers import ParameterContainer +from opto.trace.containers import ParameterContainer, trainable_method from opto.trace.nodes import ParameterNode +from opto.trace.projections import Projection, BlackCodeFormatter def model(cls): @@ -13,22 +14,61 @@ def model(cls): """ class ModelWrapper(cls, Module): - def model_dump(self, filename): - methods = [ - method for name, method in cls.__dict__.items() - if inspect.isfunction(method) - ] - - with open(filename, "w") as f: - f.write(f"class {cls.__name__}:\n") - - for i, method in enumerate(methods): - source = inspect.getsource(method) + def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): + """Dump the model's source code to a file, including all methods and attributes. + Ignores dunder methods unless they were overridden by the user. + """ + trace_model_body = f"class {cls.__name__}:\n" + + # Get all members of the class + all_members = inspect.getmembers(self) + cls_members = inspect.getmembers(cls) + cls_member_names = [m[0] for m in cls_members] + + # Filter out dunder methods unless they were overridden + filtered_members = [] + for name, member in all_members: + # Skip internal trace reserved members + if name.startswith('__TRACE_RESERVED_'): + continue + + if name not in cls_member_names: + continue + + # Include if it's not a dunder method or if it was overridden + if not name.startswith('__'): + filtered_members.append((name, member)) + elif name.startswith('__'): + # For dunder methods, check if they were overridden + try: + if hasattr(member, '__qualname__') and member.__qualname__.split('.')[0] == cls.__name__: + filtered_members.append((name, member)) + except (AttributeError, TypeError): + # Skip if we can't determine if it was overridden + continue + + # Process each member + for i, (name, member) in enumerate(filtered_members): + if 'FunModule' in str(member): + # Handle methods + source = member.parameter.data + source = textwrap.dedent(source) + indented = textwrap.indent(source, " ") + trace_model_body += indented + else: # this is a class method + source = inspect.getsource(member) source = textwrap.dedent(source) indented = textwrap.indent(source, " ") - f.write(indented) - if i < len(methods) - 1: - f.write("\n") # only one newline between methods + trace_model_body += indented + + if i < len(all_members) - 1: + trace_model_body += "\n" # only one newline between members + + if projection is not None: + trace_model_body = projection.project(trace_model_body) + + with open(filename, "w") as f: + f.write(trace_model_body) return ModelWrapper From 0b0824c7080d4c0bb45c53be26275cfdda964ce4 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 10 Jun 2025 17:24:05 -0700 Subject: [PATCH 024/172] update signature for projection --- opto/trace/projections/projections.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opto/trace/projections/projections.py b/opto/trace/projections/projections.py index f1a802c8..9be4227c 100644 --- a/opto/trace/projections/projections.py +++ b/opto/trace/projections/projections.py @@ -1,4 +1,4 @@ -from opto.trace.nodes import ParameterNode +from typing import Any class Projection: @@ -9,7 +9,7 @@ class Projection: def __init__(self, *args, **kwargs): pass - def __call__(self, x: ParameterNode) -> ParameterNode: + def __call__(self, x: Any) -> Any: """ Call the projection method on the parameter node `x`. @@ -21,7 +21,7 @@ def __call__(self, x: ParameterNode) -> ParameterNode: """ return self.project(x) - def project(self, x: ParameterNode) -> ParameterNode: + def project(self, x: Any) -> Any: """ Project the parameter node `x` onto the feasible set. """ From f38f34af9543994cdb6d2d8f9753a3fba263855b Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 10 Jun 2025 21:29:12 -0500 Subject: [PATCH 025/172] Make prompt templates as atributes of the trainer classes --- opto/trainer/algorithms/UCBsearch.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index d6460418..a589fcdc 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -399,6 +399,11 @@ class HybridUCB_LLM(MinibatchAlgorithm): If the buffer is full, evicts the candidate with the lowest UCB score. """ + # LLM prompt templates as class attributes for easy customization + SYSTEM_PROMPT_TEMPLATE = "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON." + + USER_PROMPT_TEMPLATE = "Here are some current candidates from the search buffer and their statistics:\n{candidates}\n\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\n{example_structure}\n\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values." + def __init__(self, agent: trace.Module, optimizer, @@ -535,8 +540,8 @@ def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, st example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + {"role": "system", "content": self.SYSTEM_PROMPT_TEMPLATE}, + {"role": "user", "content": self.USER_PROMPT_TEMPLATE.format(candidates=serializable_candidate_summaries, example_structure=example_param_structure_json_str)} ] print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") @@ -873,6 +878,11 @@ class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): UCB Search Algorithm that uses LLM function approximation to select candidates. """ + # LLM prompt templates as class attributes for easy customization + SYSTEM_PROMPT_TEMPLATE = "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON." + + USER_PROMPT_TEMPLATE = "Here are some current candidates from the search buffer and their statistics:\n{candidates}\n\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\n{example_structure}\n\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values." + def __init__(self, llm_model, *args, **kwargs): super().__init__(*args, **kwargs) self.llm_model = llm_model @@ -916,8 +926,8 @@ def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, st example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + {"role": "system", "content": self.SYSTEM_PROMPT_TEMPLATE}, + {"role": "user", "content": self.USER_PROMPT_TEMPLATE.format(candidates=serializable_candidate_summaries, example_structure=example_param_structure_json_str)} ] print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") From 15057bd5a476d73efb7ad6cbdc81918af9750e7a Mon Sep 17 00:00:00 2001 From: windweller Date: Sat, 14 Jun 2025 16:45:42 -0700 Subject: [PATCH 026/172] improve `model_dump` to handle `node` attributes and unpack those as well. Added test cases --- opto/trace/modules.py | 15 ++++++ tests/unit_tests/test_modules.py | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index bdfbcda3..7b8909bc 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -64,6 +64,21 @@ def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): if i < len(all_members) - 1: trace_model_body += "\n" # only one newline between members + # Replace node initializations with their current values + # WARNING: there might be corner cases that this static analysis does not cover + import re + node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' + + def replace_node(match): + attr_name = match.group(1) + if hasattr(self, attr_name): + attr = getattr(self, attr_name) + if hasattr(attr, 'data'): + return f"self.{attr_name} = {attr.data}" + return match.group(0) # Return original if replacement not possible + + trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) + if projection is not None: trace_model_body = projection.project(trace_model_body) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 8cc19893..ff05318d 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -156,3 +156,96 @@ def test_multiple_inheritance(): result = child.forward(1) assert result._data == 2 + +# Test cases for model_dump +@model +class DummyClass: + def __init__(self): + super().__init__() + self._param = node(1, trainable=True) + self.regular_attr = "test" + + @bundle(trainable=True) + def regular_method(self, x): + return x + + def __str__(self): + return "DummyClass" + + def __custom__(self): + return "custom" + +@model +class ComplexClass: + def __init__(self): + super().__init__() + self._param = node(1, trainable=True) + self._nested = DummyClass() + + @bundle(trainable=True) + def complex_method(self, x): + return self._nested.regular_method(x) + + def __str__(self): + return "ComplexClass" + +def test_model_dump_basic(): + dummy = DummyClass() + dummy._param._data = 42 # Change the node value + temp_file = "temp_dummy.py" + try: + dummy.model_dump(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class DummyClass:" in content + # Check if regular method is present + assert "def regular_method" in content + # Check if __str__ is present (overridden dunder) + assert "def __str__" in content + # Check if __custom__ is present (custom dunder) + assert "def __custom__" in content + # Check if regular attribute is present + assert "regular_attr" in content + # Check if node initialization was replaced with current value + assert "self._param = 42" in content + assert "self._param = node(1" not in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_model_dump_complex(): + complex_obj = ComplexClass() + temp_file = "temp_complex.py" + try: + complex_obj.model_dump(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class ComplexClass:" in content + # Check if complex method is present + assert "def complex_method" in content + # Check if __str__ is present + assert "def __str__" in content + # Check if nested class reference is in the method + assert "self._nested.regular_method" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_model_dump_with_projection(): + dummy = DummyClass() + temp_file = "temp_dummy_formatted.py" + try: + # Test with BlackCodeFormatter + from opto.trace.projections import BlackCodeFormatter + dummy.model_dump(temp_file, projection=BlackCodeFormatter()) + with open(temp_file, "r") as f: + content = f.read() + # Check if content is properly formatted + assert "class DummyClass:" in content + assert "def regular_method" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + From cfaddeac38879d1dd25f1e6eb7489ba2201820ee Mon Sep 17 00:00:00 2001 From: windweller Date: Sat, 14 Jun 2025 17:02:50 -0700 Subject: [PATCH 027/172] fix a bug, added new test cases --- opto/trace/modules.py | 11 +++- tests/unit_tests/test_modules.py | 89 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 7b8909bc..ee779100 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -41,7 +41,10 @@ def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): elif name.startswith('__'): # For dunder methods, check if they were overridden try: - if hasattr(member, '__qualname__') and member.__qualname__.split('.')[0] == cls.__name__: + print(cls.__name__, "<>", member.__qualname__) + # MixedClass <> test_model_dump_mixed_trainable..MixedClass.__init__ + # if we wrap it inside a function, the qualname is different than when we dont + if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: filtered_members.append((name, member)) except (AttributeError, TypeError): # Skip if we can't determine if it was overridden @@ -49,9 +52,13 @@ def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): # Process each member for i, (name, member) in enumerate(filtered_members): + print(name, member) if 'FunModule' in str(member): # Handle methods - source = member.parameter.data + if member.parameter is not None: + source = member.parameter.data + else: + source = member.info['source'] source = textwrap.dedent(source) indented = textwrap.indent(source, " ") trace_model_body += indented diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index ff05318d..46971917 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -249,3 +249,92 @@ def test_model_dump_with_projection(): if os.path.exists(temp_file): os.remove(temp_file) +@model +class NonTrainableClass: + def __init__(self): + super().__init__() + self._param = node(1, trainable=False) + self._param2 = node(2, trainable=False) + self.regular_attr = "test" + + @bundle(trainable=False) + def non_trainable_method(self, x): + return x + + @bundle(trainable=False) + def another_non_trainable(self, y): + return y + 1 + +def test_model_dump_non_trainable(): + obj = NonTrainableClass() + obj._param._data = 10 # Change node value + obj._param2._data = 20 # Change another node value + temp_file = "temp_non_trainable.py" + try: + obj.model_dump(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class NonTrainableClass:" in content + # Check if node initializations were replaced with current values + assert "self._param = 10" in content + assert "self._param2 = 20" in content + # Verify no node() calls remain + assert "node(" not in content + # Verify no bundle decorators remain + assert "@bundle" not in content + # Check if methods are present but without decorators + assert "def non_trainable_method" in content + assert "def another_non_trainable" in content + # Check if regular attribute is present + assert "regular_attr" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + +def test_model_dump_mixed_trainable(): + + @model + class MixedClass: + def __init__(self): + super().__init__() + self._trainable = node(1, trainable=True) + self._non_trainable = node(2, trainable=False) + self.regular_attr = "test" + + @bundle(trainable=True) + def trainable_method(self, x): + return x + + @bundle(trainable=False) + def non_trainable_method(self, y): + return y + 1 + + + obj = MixedClass() + obj._trainable._data = 100 + obj._non_trainable._data = 200 + + temp_file = "temp_mixed.py" + try: + obj.model_dump(temp_file) + with open(temp_file, "r") as f: + content = f.read() + # Check if class definition is present + assert "class MixedClass:" in content + # Check if all node initializations were replaced + assert "self._trainable = 100" in content + assert "self._non_trainable = 200" in content + # Verify no node() calls remain + assert "node(" not in content + # Verify no bundle decorators remain + assert "@bundle" not in content + # Check if methods are present but without decorators + assert "def trainable_method" in content + assert "def non_trainable_method" in content + # Check if regular attribute is present + assert "regular_attr" in content + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + From e186ace05f9d24f9bc0c4276898e03d78f7a7fa4 Mon Sep 17 00:00:00 2001 From: Xavier Daull Date: Mon, 16 Jun 2025 20:12:05 +0200 Subject: [PATCH 028/172] ADDED: multi-LLM support via LLMFactory (fully backward compatible) and implementation demonstration in OptoPrimeMulti and associated test --- opto/optimizers/optoprime.py | 7 +- opto/optimizers/optoprimemulti.py | 82 ++++++- opto/utils/llm.py | 48 +++- .../test_optimizer_optoprimemulti.py | 209 +++++++++++++++++- 4 files changed, 337 insertions(+), 9 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6ac4ce95..a804ed88 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -479,7 +479,12 @@ def construct_update_dict( if node.trainable and node.py_name in suggestion: try: from black import format_str, FileMode - formatted_suggestion = suggestion[node.py_name] + # Handle code parameters specially + if "__code" in node.py_name and "code" in suggestion: + formatted_suggestion = suggestion["code"] + else: + formatted_suggestion = suggestion[node.py_name] + # formatted_suggestion = suggestion[node.py_name] # use black formatter for code reformatting if type(formatted_suggestion) == str and 'def' in formatted_suggestion: formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py index 6134824f..19dadb70 100644 --- a/opto/optimizers/optoprimemulti.py +++ b/opto/optimizers/optoprimemulti.py @@ -19,6 +19,8 @@ def __init__( generation_technique: str = "temperature_variation", selection_technique: str = "best_of_n", experts_list: Optional[List[str]] = None, + llm_profiles: Optional[List[str]] = None, # List of LLM profiles to use + llm_weights: Optional[List[float]] = None, # Weights for each LLM (for weighted selection) **kwargs, ): super().__init__(*args, **kwargs) @@ -31,6 +33,42 @@ def __init__( self.selection_technique = selection_technique self.experts_list = experts_list + # NEW: Multiple LLM support + self.llm_profiles = llm_profiles + self.llm_weights = llm_weights or [1.0] * len(llm_profiles) if llm_profiles else None + self._llm_instances = {} # Cache for LLM instances + + def _get_llm_for_profile(self, profile: str = None): + """Get LLM instance for a profile, with caching.""" + if profile is None: + return self.llm # Use default LLM + + if profile not in self._llm_instances: + try: + from opto.utils.llm import LLMFactory + self._llm_instances[profile] = LLMFactory.get_llm(profile) + except Exception as e: + # Fallback to default LLM if profile creation fails + import warnings + warnings.warn(f"Failed to create LLM for profile '{profile}': {e}. Using default LLM.") + return self.llm + + return self._llm_instances[profile] + + def _get_llms_for_generation(self, num_responses: int): + """Get list of LLMs to use for generation.""" + if self.llm_profiles is None or len(self.llm_profiles) == 0: + # Fallback to single LLM (existing behavior) + return [self.llm] * num_responses + + # Distribute responses across multiple LLMs + llms = [] + for i in range(num_responses): + profile_idx = i % len(self.llm_profiles) + profile = self.llm_profiles[profile_idx] + llm = self._get_llm_for_profile(profile) + llms.append(llm) + def call_llm( self, system_prompt: str, @@ -39,20 +77,24 @@ def call_llm( max_tokens: int = 4096, num_responses: int = 1, temperature: float = 0.0, + llm = None, # NEW: Optional specific LLM to use ) -> List[str]: - """Call the LLM with a prompt and return multiple responses.""" + """Given a prompt, returns multiple candidate answers.""" # if verbose not in (False, "output"): # print("Prompt\n", system_prompt + user_prompt) + # Use provided LLM or fall back to default + active_llm = llm or self.llm + messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] try: - if hasattr(self.llm, "create"): + if hasattr(active_llm, "create"): # Standard OpenAI/LangChain style - response = self.llm.create( + response = active_llm.create( messages=messages, response_format={"type": "json_object"}, max_tokens=max_tokens, @@ -62,7 +104,7 @@ def call_llm( else: # Fallback for LiteLLM (callable) or other interfaces # e.g., LiteLLM(messages, max_tokens=…, n=…, temperature=…) - response = self.llm( + response = active_llm( messages, max_tokens=max_tokens, n=num_responses, @@ -165,6 +207,36 @@ def generate_candidates( generation_technique = generation_technique.lower() + if self.llm_profiles is not None and len(self.llm_profiles) > 0 and generation_technique == "multi_llm": + llms = self._get_llms_for_generation(num_responses) + temperatures = [temp_max - i * (temp_max - temp_min) / max(1, num_responses - 1) for i in range(num_responses)] + + # Prepare arguments for parallel execution + arg_dicts = [] + for i, (llm, temp) in enumerate(zip(llms, temperatures)): + profile_name = self.llm_profiles[i % len(self.llm_profiles)] if self.llm_profiles else "default" + modified_system_prompt = f"{system_prompt}\n\n[Using {profile_name} model for diverse perspective]" + + arg_dicts.append(dict( + system_prompt=modified_system_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=max_tokens, + num_responses=1, + temperature=temp, + llm=llm # Use specific LLM + )) + + # Execute in parallel + try: + parallel_results = self._parallel_call_llm(arg_dicts) + candidates.extend(parallel_results) + except Exception as e: + if verbose: + print(f"Error in multi_llm mode: {e} – falling back to temperature variation") + generation_technique = "temperature_variation" + candidates = [] + if generation_technique == "self_refinement": # Generate solutions by refining previous ones for i in range(num_responses): @@ -292,7 +364,7 @@ def generate_candidates( print("Warning: Failed to generate any candidates") if self.log is not None: - self.log.append({"system_prompt": system_prompt, "user_prompt": user_prompt, "response": candidates, "generation_technique": generation_technique}) + self.log.append({"system_prompt": system_prompt, "user_prompt": user_prompt, "response": candidates, "generation_technique": generation_technique, "llm_profiles": self.llm_profiles}) # only build a problem instance if we actually have one pi = self.problem_instance(summary) if summary is not None else {} self.summary_log.append({"problem_instance": pi, "summary": summary}) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index a84e4865..5039b266 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -239,6 +239,45 @@ def create(self, **config: Any): "CustomLLM": CustomLLM, } +class LLMFactory: + """Factory for creating LLM instances with predefined profiles.""" + + # Default profiles for different use cases + _profiles = { + 'default': {'backend': 'LiteLLM', 'params': {'model': 'gpt-4o-mini'}}, + 'premium': {'backend': 'LiteLLM', 'params': {'model': 'gpt-4'}}, + 'cheap': {'backend': 'LiteLLM', 'params': {'model': 'gpt-4o-mini'}}, + 'fast': {'backend': 'LiteLLM', 'params': {'model': 'gpt-3.5-turbo-mini'}}, + 'reasoning': {'backend': 'LiteLLM', 'params': {'model': 'o1-mini'}}, + } + + @classmethod + def get_llm(cls, profile: str = 'default') -> AbstractModel: + """Get an LLM instance for the specified profile.""" + if profile not in cls._profiles: + raise ValueError(f"Unknown profile '{profile}'. Available profiles: {list(cls._profiles.keys())}") + + config = cls._profiles[profile] + backend_cls = _LLM_REGISTRY[config['backend']] + return backend_cls(**config['params']) + + @classmethod + def register_profile(cls, name: str, backend: str, **params): + """Register a new LLM profile.""" + cls._profiles[name] = {'backend': backend, 'params': params} + + @classmethod + def list_profiles(cls): + """List all available profiles.""" + return list(cls._profiles.keys()) + + @classmethod + def get_profile_info(cls, profile: str = None): + """Get information about a profile or all profiles.""" + if profile: + return cls._profiles.get(profile) + return cls._profiles + class LLM: """ A unified entry point for all supported LLM backends. @@ -248,8 +287,15 @@ class LLM: llm = LLM() # or override explicitly llm = LLM(backend="AutoGen", config_list=my_configs) + # or use predefined profiles + llm = LLM(profile="premium") # Use premium model + llm = LLM(profile="cheap") # Use cheaper model + llm = LLM(profile="reasoning") # Use reasoning/thinking model """ - def __new__(cls, *args, backend: str = None, **kwargs): + def __new__(cls, *args, profile: str = None, backend: str = None, **kwargs): + # New: if profile is specified, use LLMFactory + if profile: + return LLMFactory.get_llm(profile) # Decide which backend to use name = backend or os.getenv("TRACE_DEFAULT_LLM_BACKEND", "LiteLLM") try: diff --git a/tests/llm_optimizers_tests/test_optimizer_optoprimemulti.py b/tests/llm_optimizers_tests/test_optimizer_optoprimemulti.py index c9acd708..978ae302 100644 --- a/tests/llm_optimizers_tests/test_optimizer_optoprimemulti.py +++ b/tests/llm_optimizers_tests/test_optimizer_optoprimemulti.py @@ -1,6 +1,7 @@ import json import pytest from opto.optimizers.optoprimemulti import OptoPrimeMulti +from opto.utils.llm import LLMFactory from opto.trace.propagators import GraphPropagator from opto.trace.nodes import ParameterNode from opto.trace import bundle, node, GRAPH @@ -25,6 +26,18 @@ def __call__(self, messages, max_tokens=None, response_format=None): # fallback single-call (not used in multi) return self.create(messages, response_format, max_tokens, 1, 0) +class MockLLMFactory: + """Mock LLMFactory for testing multi-LLM functionality""" + @staticmethod + def get_llm(profile): + # Return different dummy LLMs for different profiles + profile_responses = { + 'cheap': [f"cheap_{profile}_response"], + 'premium': [f"premium_{profile}_response"], + 'default': [f"default_{profile}_response"], + } + return DummyLLM(responses=[profile_responses.get(profile, ["default_response"])]) + @pytest.fixture def parameter_node(): # Minimal dummy ParameterNode @@ -40,6 +53,16 @@ def default_optimizer(parameter_node): assert isinstance(opt.propagator, GraphPropagator) return opt +@pytest.fixture +def multi_llm_optimizer(parameter_node): + """Optimizer configured for multi-LLM testing""" + dummy = DummyLLM(responses=[["{\\\"suggestion\\\": {}}"]]) + opt = OptoPrimeMulti([parameter_node], + llm_profiles=['cheap', 'premium', 'default'], + generation_technique='multi_llm') + opt.llm = dummy + return opt + def test_call_llm_returns_list(default_optimizer): opt = default_optimizer # Prepare dummy response @@ -48,11 +71,25 @@ def test_call_llm_returns_list(default_optimizer): assert isinstance(results, list) assert results == ["resp1", "resp2"] +def test_call_llm_with_specific_llm(default_optimizer): + """Test that call_llm accepts and uses a specific LLM instance""" + opt = default_optimizer + specific_llm = DummyLLM(responses=[["specific_response"]]) + + # Call with specific LLM + results = opt.call_llm("sys", "usr", llm=specific_llm, num_responses=1) + assert results == ["specific_response"] + + # Verify specific_llm was called, not the default + assert len(specific_llm.call_args) == 1 + assert len(opt.llm.call_args) == 0 # Default LLM should not be called + @pytest.mark.parametrize("gen_tech", [ "temperature_variation", "self_refinement", "iterative_alternatives", - "multi_experts"] + "multi_experts", + "multi_llm"] ) def test_generate_candidates_length(default_optimizer, gen_tech, capsys): opt = default_optimizer @@ -65,6 +102,55 @@ def test_generate_candidates_length(default_optimizer, gen_tech, capsys): assert isinstance(cands, list) assert len(cands) == 3 +def test_multi_llm_initialization(): + """Test OptoPrimeMulti initialization with multi-LLM parameters""" + param = ParameterNode(name='test', value=1) + profiles = ['cheap', 'premium', 'default'] + weights = [0.5, 1.5, 1.0] + + opt = OptoPrimeMulti([param], + llm_profiles=profiles, + llm_weights=weights, + generation_technique='multi_llm') + + assert opt.llm_profiles == profiles + assert opt.llm_weights == weights + assert opt._llm_instances == {} # Should start empty + +def test_get_llm_for_profile(multi_llm_optimizer, monkeypatch): + """Test LLM profile retrieval and caching""" + opt = multi_llm_optimizer + + # Mock LLMFactory + monkeypatch.setattr('opto.utils.llm.LLMFactory', MockLLMFactory) + + # First call should create and cache + llm1 = opt._get_llm_for_profile('cheap') + assert 'cheap' in opt._llm_instances + + # Second call should return cached instance + llm2 = opt._get_llm_for_profile('cheap') + assert llm1 is llm2 + + # None profile should return default LLM + default_llm = opt._get_llm_for_profile(None) + assert default_llm is opt.llm + +def test_get_llms_for_generation(multi_llm_optimizer, monkeypatch): + """Test LLM distribution for generation""" + opt = multi_llm_optimizer + # Patch the import location where it's actually used + monkeypatch.setattr('opto.optimizers.optoprimemulti.LLMFactory', MockLLMFactory) + + llms = opt._get_llms_for_generation(5) + assert len(llms) == 5 + + # Should cycle through profiles: cheap, premium, default, cheap, premium + expected_profiles = ['cheap', 'premium', 'default', 'cheap', 'premium'] + for i, llm in enumerate(llms): + expected_profile = expected_profiles[i] + assert expected_profile in opt._llm_instances + @pytest.mark.parametrize("sel_tech,method_name", [ ("moa", "_select_moa"), ("majority", "_select_majority"), @@ -85,6 +171,25 @@ def test_select_candidate_calls_correct_method(default_optimizer, sel_tech, meth result = opt.select_candidate(cands, selection_technique=sel_tech) assert result == "c" +def test_multi_llm_generation_fallback(multi_llm_optimizer, monkeypatch): + """Test that multi_llm generation falls back gracefully on error""" + opt = multi_llm_optimizer + + # Mock LLMFactory to raise exception + def failing_get_llm(profile): + raise Exception("LLM creation failed") + + monkeypatch.setattr(MockLLMFactory, 'get_llm', failing_get_llm) + monkeypatch.setattr('opto.utils.llm.LLMFactory', MockLLMFactory) + + # Should fall back to temperature_variation + responses = [["fallback1"], ["fallback2"], ["fallback3"]] + opt.llm = DummyLLM(responses=responses) + + cands = opt.generate_candidates(None, "sys", "usr", num_responses=3, + generation_technique="multi_llm", verbose=True) + assert len(cands) == 3 + def test_integration_step_updates(default_optimizer, parameter_node): opt = default_optimizer # Dummy parameter_node initial value @@ -105,6 +210,83 @@ def test_default_model_name(default_optimizer): assert 'gpt-4.1-nano' in model_name +def test_multi_llm_step_integration(multi_llm_optimizer, parameter_node, monkeypatch): + """Test full integration of multi-LLM optimization step""" + opt = multi_llm_optimizer + monkeypatch.setattr('opto.utils.llm.LLMFactory', MockLLMFactory) + + parameter_node._data = 0 + + # Mock multiple LLM responses for multi_llm generation + suggestion = {"x": 42} + response_str = json.dumps({"reasoning": "ok", "answer": "", "suggestion": suggestion}) + + # Each profile should return a response + cheap_llm = DummyLLM(responses=[[response_str]]) + premium_llm = DummyLLM(responses=[[response_str]]) + default_llm = DummyLLM(responses=[[response_str]]) + + opt._llm_instances = { + 'cheap': cheap_llm, + 'premium': premium_llm, + 'default': default_llm + } + + # Override _parallel_call_llm to return mock responses + def mock_parallel_call(arg_dicts): + return [response_str] * len(arg_dicts) + + opt._parallel_call_llm = mock_parallel_call + + # Run optimization step + update = opt._step(verbose=False, generation_technique='multi_llm') + assert isinstance(update, dict) + +def test_llm_weights_handling(): + """Test that LLM weights are properly handled""" + param = ParameterNode(name='test', value=1) + + # Test with explicit weights + profiles = ['cheap', 'premium'] + weights = [0.3, 0.7] + opt1 = OptoPrimeMulti([param], llm_profiles=profiles, llm_weights=weights) + assert opt1.llm_weights == weights + + # Test with automatic weights (should default to 1.0 for each profile) + opt2 = OptoPrimeMulti([param], llm_profiles=profiles) + assert opt2.llm_weights == [1.0, 1.0] + + # Test without profiles (should be None) + opt3 = OptoPrimeMulti([param]) + assert opt3.llm_weights is None + +def test_multi_llm_logging(multi_llm_optimizer, monkeypatch): + """Test that multi-LLM usage is properly logged""" + opt = multi_llm_optimizer + opt.log = [] # Enable logging + + # Manually set LLM instances to avoid import issues + opt._llm_instances = { + 'cheap': DummyLLM(responses=[["response1"]]), + 'premium': DummyLLM(responses=[["response2"]]), + 'default': DummyLLM(responses=[["response3"]]) + } + + # Override _parallel_call_llm to return mock responses + def mock_parallel_call(arg_dicts): + return ["response1", "response2", "response3"] + + opt._parallel_call_llm = mock_parallel_call + + cands = opt.generate_candidates(None, "sys", "usr", num_responses=3, + generation_technique="multi_llm") + + # Check that logging includes llm_profiles + assert len(opt.log) > 0 + log_entry = opt.log[-1] + assert 'llm_profiles' in log_entry + assert log_entry['llm_profiles'] == ['cheap', 'premium', 'default'] + def user_code(output): if output < 0: return "Success." @@ -115,7 +297,8 @@ def user_code(output): "temperature_variation", "self_refinement", "iterative_alternatives", - "multi_experts" + "multi_experts", + "multi_llm" ]) @pytest.mark.parametrize("sel_tech", [ "moa", @@ -150,3 +333,25 @@ def my_fun(x): print(f"Function updated: old value: {str(old_func_value)}, new value: {str(new_func_value)}") +def test_backwards_compatibility(): + """Test that existing OptoPrimeMulti usage continues to work without changes""" + param = ParameterNode(name='test', value=1) + + # Old-style initialization should work exactly as before + opt = OptoPrimeMulti([param], + num_responses=3, + generation_technique="temperature_variation", + selection_technique="best_of_n") + + # New attributes should have sensible defaults + assert opt.llm_profiles is None + assert opt.llm_weights is None + assert opt._llm_instances == {} + + # Should fall back to single LLM behavior + llms = opt._get_llms_for_generation(3) + assert len(llms) == 3 + assert all(llm is opt.llm for llm in llms) + + # Profile retrieval should return default LLM for None + assert opt._get_llm_for_profile(None) is opt.llm \ No newline at end of file From 7a12c2f84710f063d8921210a3eac3ae8d71e2c3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 16 Jun 2025 21:34:04 +0000 Subject: [PATCH 029/172] Fix a bug of missing default test_dataset. --- opto/trainer/algorithms/basic_algorithms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 66596580..9baa09e5 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -110,6 +110,7 @@ def train(self, log_frequency = log_frequency or eval_frequency # frequency of logging (default to eval_frequency) num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided use_asyncio = self._use_asyncio(num_threads) # Evaluate the agent before learning From 239194dc87c2ab475d22de02012b5cbc17d4664a Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 16 Jun 2025 22:30:57 +0000 Subject: [PATCH 030/172] Set to use default LLM instead of LiteLLM. --- examples/search_algo_example.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index 32c6db70..09df72d2 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -18,7 +18,7 @@ from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm, HybridUCB_LLM, UCBSearchFunctionApproximationAlgorithm from opto.trainer.guide import AutoGuide from opto.trainer.loggers import DefaultLogger -from opto.utils.llm import LLM, LiteLLM +from opto.utils.llm import LLM # Set default model # os.environ["TRACE_LITELLM_MODEL"] = "vertex_ai/gemini-2.0-flash" @@ -41,7 +41,7 @@ def __init__(self, super().__init__() self.system_prompt = trace.node(system_prompt, trainable=True) self.user_prompt_template = trace.node(user_prompt_template, trainable=True) - self.llm = llm or LiteLLM(model="gpt-3.5-turbo") + self.llm = llm or LLM(model="gpt-3.5-turbo") @trace.bundle() def call_llm(self, system_prompt: str, user_prompt: str) -> str: @@ -85,7 +85,7 @@ def __init__(self, model: str = "gpt-4o-mini"): model: The LLM model to use for evaluation """ super().__init__() - self.guide_llm = LiteLLM(model=model) + self.guide_llm = LLM(model=model) self.system_prompt = "You are an expert math teacher evaluating student answers." self.judge_prompt_template = ( "Carefully review the following three distinct sections:\n\n" @@ -252,7 +252,7 @@ def main(): # Set environment variables os.environ["TRACE_LITELLM_MODEL"] = args.trace_model - + # Set random seed np.random.seed(args.seed) @@ -283,7 +283,7 @@ def main(): # Initialize components print("Initializing Agent, Guide, Optimizer, Algorithm...") - student_llm = LiteLLM(model=args.student_model) + student_llm = LLM(model=args.student_model) agent = Learner(llm=student_llm) train_guide = TeacherGuide(model=args.teacher_model) @@ -291,7 +291,7 @@ def main(): optimizer = OptoPrime(agent.parameters()) logger = SimpleLogger() - + # Create algorithm if args.algorithm_type == 'minibatch': algorithm = MinibatchAlgorithm( From 874248d9a9acb9b32f325eb886c641c25872bdd6 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 16 Jun 2025 22:55:29 +0000 Subject: [PATCH 031/172] Remove necessary dependency on LiteLLM and remove using vertex as default. --- examples/search_algo_example.py | 11 +++++----- opto/trainer/algorithms/UCBsearch.py | 30 ++++++++++------------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index 09df72d2..537727eb 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -199,11 +199,11 @@ def main(): help='Number of test samples') # LLM Model parameters - parser.add_argument('--trace_model', type=str, default='vertex_ai/gemini-2.0-flash', + parser.add_argument('--trace_model', type=str, default=None, help='Model to use for trace operations') - parser.add_argument('--student_model', type=str, default='vertex_ai/gemini-2.0-flash', + parser.add_argument('--student_model', type=str, default=None, help='Model to use for student agent') - parser.add_argument('--teacher_model', type=str, default='vertex_ai/gemini-2.0-flash', + parser.add_argument('--teacher_model', type=str, default=None, help='Model to use for teacher guide') # Training parameters @@ -251,8 +251,9 @@ def main(): args = parser.parse_args() # Set environment variables - os.environ["TRACE_LITELLM_MODEL"] = args.trace_model - + if args.trace_model: + os.environ["TRACE_LITELLM_MODEL"] = args.trace_model + # Set random seed np.random.seed(args.seed) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index a589fcdc..e1a852e8 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -12,7 +12,7 @@ from opto.trainer.utils import async_run # Assuming print_color is in utils from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful -from opto.utils.llm import LiteLLM # For the selector LLM +from opto.utils.llm import LLM # For the selector LLM from opto.trace.nodes import ParameterNode import warnings @@ -399,18 +399,13 @@ class HybridUCB_LLM(MinibatchAlgorithm): If the buffer is full, evicts the candidate with the lowest UCB score. """ - # LLM prompt templates as class attributes for easy customization - SYSTEM_PROMPT_TEMPLATE = "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON." - - USER_PROMPT_TEMPLATE = "Here are some current candidates from the search buffer and their statistics:\n{candidates}\n\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\n{example_structure}\n\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values." - def __init__(self, agent: trace.Module, optimizer, max_buffer_size: int = 10, ucb_exploration_factor: float = 1.0, alpha: float = 0.7, - llm_model: str = "vertex_ai/gemini-2.0-flash", + llm_model: str = None, logger=None, num_threads: int = None, *args, @@ -430,8 +425,8 @@ def __init__(self, self._total_evaluations_tracker = 0 - # Initialize LiteLLM - self.llm = LiteLLM(model=self.llm_model) + # Initialize LLM + self.llm = LLM(model=self.llm_model) print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: @@ -540,8 +535,8 @@ def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, st example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} prompt_messages = [ - {"role": "system", "content": self.SYSTEM_PROMPT_TEMPLATE}, - {"role": "user", "content": self.USER_PROMPT_TEMPLATE.format(candidates=serializable_candidate_summaries, example_structure=example_param_structure_json_str)} + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} ] print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") @@ -878,15 +873,10 @@ class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): UCB Search Algorithm that uses LLM function approximation to select candidates. """ - # LLM prompt templates as class attributes for easy customization - SYSTEM_PROMPT_TEMPLATE = "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON." - - USER_PROMPT_TEMPLATE = "Here are some current candidates from the search buffer and their statistics:\n{candidates}\n\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\n{example_structure}\n\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values." - def __init__(self, llm_model, *args, **kwargs): super().__init__(*args, **kwargs) self.llm_model = llm_model - self.llm = LiteLLM(model=self.llm_model) + self.llm = LLM(model=self.llm_model) print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") def select(self, buffer): @@ -926,13 +916,13 @@ def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, st example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} prompt_messages = [ - {"role": "system", "content": self.SYSTEM_PROMPT_TEMPLATE}, - {"role": "user", "content": self.USER_PROMPT_TEMPLATE.format(candidates=serializable_candidate_summaries, example_structure=example_param_structure_json_str)} + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} ] print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - llm_response = self.llm(prompt_messages) + llm_response = self.llm(messages=prompt_messages) llm_response_str = llm_response.choices[0].message.content if not llm_response_str: From 36296d0483aad1db5844a7bcbf0c3dc55cea0e31 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 16 Jun 2025 23:01:02 +0000 Subject: [PATCH 032/172] Remove LiteLLM dependency in gsm8k example --- examples/gsm8k_trainer_example.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index 369eeec4..f9524dc0 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -4,7 +4,7 @@ from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrime from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm -from opto.trainer.loggers import DefaultLogger, TensorboardLogger +from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @@ -56,6 +56,7 @@ def main(): num_epochs = 1 batch_size = 1 eval_frequency = -1 + num_threads = 3 verbose = True teacher_model = None # use default model student_model = None # use default model @@ -71,7 +72,7 @@ def main(): agent = Learner(llm=LLM(student_model)) guide = Guide(model=LLM(teacher_model)) - optimizer = OptoPrime(agent.parameters(), llm=LiteLLM(optimizer_model)) + optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) logger = Logger(verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format @@ -86,7 +87,7 @@ def main(): batch_size=batch_size, eval_frequency=eval_frequency, test_dataset=test_dataset, - num_threads=3, + num_threads=num_threads, verbose='output' if verbose else False) From e592fa4c980c9eb4ba334d2a1fba1d2941f46688 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Tue, 17 Jun 2025 13:48:35 -0500 Subject: [PATCH 033/172] Add a wandb logger --- opto/trainer/loggers.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/opto/trainer/loggers.py b/opto/trainer/loggers.py index 5f82a4ac..19d1e553 100644 --- a/opto/trainer/loggers.py +++ b/opto/trainer/loggers.py @@ -79,6 +79,44 @@ def log(self, name, data, step, **kwargs): # Otherwise, log it as a scalar self.writer.add_scalar(name, data, step) -# TODO add wandb logger +class WandbLogger(ConsoleLogger): + """A logger that writes metrics to Weights and Biases (wandb).""" + + def __init__(self, log_dir='./logs', verbose=True, project=None, **kwargs): + super().__init__(log_dir, **kwargs) + self.verbose = verbose + # Late import to avoid dependency issues + try: + import wandb + except ImportError: + raise ImportError("wandb is required for WandbLogger. Install it with: pip install wandb") + + # Initialize wandb + self.wandb = wandb + if not wandb.run: + wandb.init(project=project, dir=log_dir, **kwargs) + + def log(self, name, data, step, **kwargs): + """Log a message to Weights and Biases. + + Args: + name: Name of the metric + data: Value of the metric + step: Current step/iteration + **kwargs: Additional arguments (not used here) + """ + if self.verbose: + super().log(name, data, step, **kwargs) + + # Log to wandb + if isinstance(data, str): + # For string data, we can log it as a custom chart or just print it + # wandb doesn't have a direct equivalent to tensorboard's add_text + # but we can log it in a structured way + self.wandb.log({f"{name}_text": data}, step=step) + else: + # For numeric data, log as scalar + self.wandb.log({name: data}, step=step) + DefaultLogger = ConsoleLogger \ No newline at end of file From 6a6a16536efd36d6d1b570a2ecb453de49f62123 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Tue, 17 Jun 2025 21:09:18 -0500 Subject: [PATCH 034/172] Deleted two UCB search algorithms in PR --- examples/search_algo_example.py | 42 +- opto/trainer/algorithms/UCBsearch.py | 671 +-------------------------- 2 files changed, 7 insertions(+), 706 deletions(-) diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index 537727eb..ea3421c8 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -2,7 +2,7 @@ import os import time import argparse -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Tuple # Third-party imports import datasets @@ -15,7 +15,7 @@ from opto.trace.modules import Module from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm -from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm, HybridUCB_LLM, UCBSearchFunctionApproximationAlgorithm +from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm from opto.trainer.guide import AutoGuide from opto.trainer.loggers import DefaultLogger from opto.utils.llm import LLM @@ -184,8 +184,8 @@ def main(): parser = argparse.ArgumentParser(description='Train agent using various algorithms') # Algorithm parameters - parser.add_argument('--algorithm_type', type=str, default='UCBSearchFunctionApproximationAlgorithm', - choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory', 'UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm'], + parser.add_argument('--algorithm_type', type=str, default='UCBsearch', + choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory', 'UCBsearch'], help='Type of algorithm to use') # Dataset parameters @@ -239,8 +239,6 @@ def main(): help='Maximum buffer size for UCB algorithms') parser.add_argument('--ucb_exploration_factor', type=float, default=1.0, help='UCB exploration factor') - parser.add_argument('--alpha', type=float, default=0.3, - help='Alpha parameter for HybridUCB_LLM (probability of UCB vs LLM path)') parser.add_argument('--num_search_iterations', type=int, default=100, help='Number of search iterations for UCB algorithms') parser.add_argument('--train_batch_size_ucb', type=int, default=2, @@ -331,27 +329,6 @@ def main(): max_buffer_size=args.max_buffer_size, ucb_exploration_factor=args.ucb_exploration_factor ) - elif args.algorithm_type == 'HybridUCB_LLM': - algorithm = HybridUCB_LLM( - agent=agent, - optimizer=optimizer, - logger=logger, - num_threads=args.num_threads, - max_buffer_size=args.max_buffer_size, - ucb_exploration_factor=args.ucb_exploration_factor, - alpha=args.alpha, - llm_model=args.trace_model - ) - elif args.algorithm_type == 'UCBSearchFunctionApproximationAlgorithm': - algorithm = UCBSearchFunctionApproximationAlgorithm( - agent=agent, - optimizer=optimizer, - logger=logger, - num_threads=args.num_threads, - max_buffer_size=args.max_buffer_size, - ucb_exploration_factor=args.ucb_exploration_factor, - llm_model=args.trace_model - ) else: raise ValueError(f"Unknown algorithm type: {args.algorithm_type}") @@ -384,7 +361,7 @@ def main(): elif args.algorithm_type == 'basicsearch': train_params["num_proposals"] = args.num_basicsearch_proposals - elif args.algorithm_type in ['UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm']: + elif args.algorithm_type == 'UCBsearch': train_params.update({ "num_search_iterations": args.num_search_iterations, "train_batch_size": args.train_batch_size_ucb, @@ -404,20 +381,13 @@ def main(): for depth, score in enumerate(metrics['best_validation_scores']): print(f" Depth {depth+1}: {score:.4f}") - elif args.algorithm_type in ['UCBsearch', 'HybridUCB_LLM', 'UCBSearchFunctionApproximationAlgorithm']: + elif args.algorithm_type == 'UCBsearch': print("\nUCB Algorithm Metrics:") if 'best_candidate_scores' in metrics and metrics['best_candidate_scores']: print(f" Best candidate scores over iterations: {len(metrics['best_candidate_scores'])} recorded") print(f" Final best candidate score: {metrics['best_candidate_scores'][-1]:.4f}") if 'buffer_avg_score' in metrics and metrics['buffer_avg_score']: print(f" Final buffer average score: {metrics['buffer_avg_score'][-1]:.4f}") - if args.algorithm_type == 'HybridUCB_LLM': - if 'llm_generation_failures' in metrics: - print(f" LLM generation failures: {metrics['llm_generation_failures']}") - if 'generation_path' in metrics: - ucb_count = metrics['generation_path'].count('ucb') - llm_count = metrics['generation_path'].count('llm') - print(f" Generation methods used - UCB: {ucb_count}, LLM: {llm_count}") print(f"Final score: {final_score:.4f}") diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index e1a852e8..0a136eff 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -1,61 +1,12 @@ import numpy as np import copy -import time import math -import json # For LLM output parsing -import re # For smart quote replacement from collections import deque from typing import Union, List, Tuple, Dict, Any, Optional -import random # Added for alpha probability - from opto import trace from opto.trainer.utils import async_run # Assuming print_color is in utils from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful -from opto.utils.llm import LLM # For the selector LLM - -from opto.trace.nodes import ParameterNode -import warnings -from black import format_str, FileMode - - -def smart_quote_replacement(text: str) -> str: - """ - Intelligently replace single quotes with double quotes for JSON parsing. - Handles the specific case where we have mixed quotes like: - {'key': "value with 'nested' quotes"} - """ - # For the specific pattern we're seeing, let's handle it step by step: - - # Step 1: Replace single quotes around keys - # Pattern: 'key': -> "key": - text = re.sub(r"'([^']*?)'(\s*:)", r'"\1"\2', text) - - # Step 2: For values that start with double quotes and contain single quotes, - # we need to escape the internal single quotes or convert them properly - - # Let's try a more direct approach for the problematic case: - # Find patterns like: "text with 'word' more text" - # We need to escape the internal single quotes - def escape_internal_quotes(match): - content = match.group(1) - # Replace single quotes inside with escaped single quotes - # Actually, for JSON we can leave single quotes as-is inside double quotes - return f'"{content}"' - - # Replace the pattern: : "content with 'quotes'" -> : "content with 'quotes'" - # (This should already be valid JSON) - - # The main issue is with the outer structure, let's fix that: - # If the string starts/ends with single quotes around the whole thing - text = text.strip() - if text.startswith("{'") and text.endswith("'}"): - # Replace the outer single quotes but preserve the content - # This is the pattern: {'str0': "content", 'str1': "more content"} - text = '{"' + text[2:-2] + '"}' - - return text - class UCBSearchAlgorithm(MinibatchAlgorithm): """ @@ -376,624 +327,4 @@ def train(self, def select(self, buffer): '''Could be subclassed to implement different selection strategies''' - return max(buffer, key=lambda c: c['ucb_score']) - - -class HybridUCB_LLM(MinibatchAlgorithm): - """ - UCB Search Algorithm with Function Approximation (LLM). - - Keeps a buffer of candidates. - In each iteration: - - With probability alpha: - 1. Picks a candidate 'a' from the buffer with the highest UCB score. - 2. Updates the optimizer with 'a's parameters. - 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a_prime'. - 4. Evaluates 'a_prime' on a validation set minibatch. - 5. Updates statistics of 'a' (based on the training minibatch). - 6. Adds 'a_prime' (with its validation stats) to the buffer. - - With probability 1-alpha: - 1. Uses an external LLM, prompted with candidates from the buffer, to generate a new candidate 'a_prime'. - 2. Evaluates 'a_prime' on a validation set minibatch. - 3. Adds 'a_prime' (with its validation stats) to the buffer. - If the buffer is full, evicts the candidate with the lowest UCB score. - """ - - def __init__(self, - agent: trace.Module, - optimizer, - max_buffer_size: int = 10, - ucb_exploration_factor: float = 1.0, - alpha: float = 0.7, - llm_model: str = None, - logger=None, - num_threads: int = None, - *args, - **kwargs): - super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) - - self.alpha = alpha - self.llm_model = llm_model - self.llm_prompt_budget_factor = 0.5 - - self.buffer = deque(maxlen=max_buffer_size) - self.max_buffer_size = max_buffer_size - self.ucb_exploration_factor = ucb_exploration_factor - - if not hasattr(self.optimizer, 'step'): - raise ValueError("Optimizer must have a 'step' method.") - - self._total_evaluations_tracker = 0 - - # Initialize LLM - self.llm = LLM(model=self.llm_model) - print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") - - def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: - """Sample a minibatch from the dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos'): - print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') - return [], [] - - dataset_size = len(dataset['inputs']) - if dataset_size == 0: - print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') - return [], [] - - actual_batch_size = min(batch_size, dataset_size) - indices = np.random.choice(dataset_size, actual_batch_size, replace=False) - xs = [dataset['inputs'][i] for i in indices] - infos = [dataset['infos'][i] for i in indices] - return xs, infos - - def _evaluate_candidate(self, - params_to_eval_dict: Dict[str, Any], - dataset: Dict[str, List[Any]], - guide, - evaluation_batch_size: int, - num_threads: Optional[int] = None - ) -> Tuple[float, int]: - """Evaluates a given set of parameters on samples from the provided dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: - print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') - return -np.inf, 0 - - original_params_backup = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - - try: - self.optimizer.update(params_to_eval_dict) - except Exception as e: - print_color(f"Error updating agent with params_to_eval_dict: {e}. Using current agent state for eval.", "red") - - eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) - - if not eval_xs: - print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') - self.optimizer.update(original_params_backup) - return -np.inf, 0 - - eval_scores = evaluate(self.agent, - guide, - eval_xs, - eval_infos, - min_score=self.min_score if hasattr(self, 'min_score') else None, - num_threads=num_threads or self.num_threads, - description=f"Evaluating candidate") - - self.optimizer.update(original_params_backup) - - avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf - eval_count = len(eval_xs) - - return float(avg_score), eval_count - - def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates UCB score for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('inf') - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - if total_tracked_evaluations == 0: - total_tracked_evaluations = 1 - - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations + 1e-9) / candidate_buffer_entry['eval_count']) - - return mean_score + exploration_term - - def _update_buffer_ucb_scores(self): - """Recalculates and updates UCB scores for all candidates in the buffer.""" - if not self.buffer: - return - - for candidate_entry in self.buffer: - candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) - - def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: - """ - Prompts an LLM with current buffer candidates to generate new string values for parameters. - Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. - """ - print_color("Attempting to generate candidate using LLM...", "blue") - if not self.buffer: - print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") - return None - - sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) - prompt_candidates = sorted_buffer - - serializable_candidate_summaries = [] - for cand_entry in prompt_candidates: - summary = { - "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, - "eval_count": cand_entry['eval_count'], - "ucb_score": round(cand_entry.get('ucb_score',0), 4), - } - serializable_candidate_summaries.append(summary) - - example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} - - prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} - ] - - print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - - llm_response = self.llm(prompt_messages) - llm_response_str = llm_response.choices[0].message.content - - if not llm_response_str: - print_color("LLM returned an empty response.", "red") - return None - - # Clean the response string - cleaned_llm_response_str = llm_response_str.strip() - if cleaned_llm_response_str.startswith("```json"): - cleaned_llm_response_str = cleaned_llm_response_str[7:] - if cleaned_llm_response_str.endswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[:-3] - elif cleaned_llm_response_str.startswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[3:] - if cleaned_llm_response_str.endswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[:-3] - cleaned_llm_response_str = cleaned_llm_response_str.strip() - - if not cleaned_llm_response_str: - print_color("LLM response was empty after cleaning markdown/whitespace.", "red") - return None - - print_color(f"Cleaned LLM response: '{cleaned_llm_response_str}'", "magenta") - - # Fix common JSON formatting issues from LLM responses - try: - llm_params_raw = json.loads(cleaned_llm_response_str) - except json.JSONDecodeError as e: - print_color(f"Initial JSON parsing failed: {e}", "yellow") - print_color("Attempting to fix JSON formatting...", "yellow") - - fixed_json_str = smart_quote_replacement(cleaned_llm_response_str) - - try: - llm_params_raw = json.loads(fixed_json_str) - print_color("Successfully fixed JSON formatting", "green") - except json.JSONDecodeError as e2: - print_color(f"Smart quote replacement failed: {e2}", "yellow") - try: - simple_fixed = cleaned_llm_response_str.replace("'", '"') - llm_params_raw = json.loads(simple_fixed) - print_color("Fallback simple replacement succeeded", "green") - except json.JSONDecodeError as e3: - print_color(f"All JSON parsing attempts failed: {e3}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - if not isinstance(llm_params_raw, dict): - print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - candidate_params_dict = self.construct_update_dict(llm_params_raw) - return candidate_params_dict - - def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" - update_dict = {} - for node in self.agent.parameters(): - if node.trainable and node.py_name in suggestion: - try: - formatted_suggestion = suggestion[node.py_name] - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - if getattr(self, 'ignore_extraction_error', False): - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e - return update_dict - - def train(self, - guide, - train_dataset: Dict[str, List[Any]], - *, - num_search_iterations: int = 100, - train_batch_size: int = 5, - evaluation_batch_size: int = 5, - ensure_improvement: bool = False, - improvement_threshold: float = 0., - eval_frequency: int = 1, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/ucb_llm_agent.pkl", - min_score_for_agent_update: Optional[float] = None, - verbose: Union[bool, str] = False, - num_threads: Optional[int] = None, - **kwargs - ) -> Tuple[Dict[str, Any], float]: - - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency - self.min_score = min_score_for_agent_update - total_samples = 0 - - metrics = { - 'best_candidate_scores': [], - 'selected_action_ucb': [], - 'new_candidate_scores': [], - 'buffer_avg_score': [], - 'buffer_avg_evals': [], - 'llm_generation_failures': 0, - 'generation_path': [] - } - - # Initial candidate evaluation - print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - - initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, train_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += initial_evals - total_samples += initial_evals - - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, - 'eval_count': initial_evals, - 'ucb_score': 0.0, - 'iteration_created': 0 - } - self.buffer.append(initial_candidate_entry) - self._update_buffer_ucb_scores() - print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') - - # Main search loop - for iteration in range(1, num_search_iterations + 1): - if not self.buffer: - print_color("Buffer is empty, stopping search.", 'red') - break - - self._update_buffer_ucb_scores() - a_prime_params_dict = None - a_prime_score = -np.inf - a_prime_evals = 0 - generation_method = "none" - - if random.random() < self.alpha: # UCB Path - generation_method = "ucb" - metrics['generation_path'].append("ucb") - if not self.buffer: - print_color(f"Iter {iteration} (UCB Path): Buffer empty, cannot select action. Skipping.", "red") - continue - - action_candidate_a = self.select(self.buffer) - - selected_mean_score = action_candidate_a['score_sum'] / action_candidate_a['eval_count'] if action_candidate_a['eval_count'] > 0 else -np.inf - print_color(f"Iter {iteration} (UCB Path): Selected action candidate (UCB: {action_candidate_a['ucb_score']:.4f}, MeanScore: {selected_mean_score:.4f} Evals: {action_candidate_a['eval_count']})", 'blue') - metrics['selected_action_ucb'].append(action_candidate_a['ucb_score']) - - self.optimizer.update(action_candidate_a['params']) - - train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) - if not train_xs: - print_color(f"Iter {iteration} (UCB Path): Training minibatch empty, skipping optimizer step.", 'yellow') - continue - - total_samples += len(train_xs) - - # Forward pass for 'a' - outputs_for_a = [] - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - outputs_for_a = async_run([self.forward]*len(train_xs), - [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], - max_workers=num_threads, - description=f"Iter {iteration} (UCB): Forward for 'a'") - else: - outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] - - scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] - for target, score, feedback in outputs_for_a: - scores_from_train.append(score) - targets_from_train.append(target) - feedbacks_from_train.append(feedback) - - if not scores_from_train: - print_color(f"Iter {iteration} (UCB Path): No outputs from forward pass for 'a'. Skipping.", 'yellow') - continue - - target_for_a = batchify(*targets_from_train) - feedback_for_a = batchify(*feedbacks_from_train).data - score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf - - self.optimizer.zero_feedback() - self.optimizer.backward(target_for_a, feedback_for_a) - - # Get a_prime by optimizer step - try: - returned_params = self.optimizer.step(bypassing=True, verbose=(verbose if isinstance(verbose, str) else 'output')) - if not isinstance(returned_params, dict) or not returned_params: - print_color(f"Iter {iteration} (UCB Path): Optimizer.step did not return a valid param dict for a_prime. Using current agent params.", 'yellow') - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - else: - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in returned_params} - - except Exception as e: - print_color(f"Iter {iteration} (UCB Path): Error during optimizer.step for a_prime: {e}. Skipping.", 'red') - continue - - # Evaluate a_prime (from UCB path) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += a_prime_evals - total_samples += a_prime_evals - - # Update stats of action_candidate_a - if score_for_a_on_train_batch > -np.inf: - action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) - action_candidate_a['eval_count'] += len(train_xs) - self._total_evaluations_tracker += len(train_xs) - - print_color(f"Iter {iteration} (UCB Path): New candidate a_prime (from UCB) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') - - else: # LLM Path - generation_method = "llm" - metrics['generation_path'].append("llm") - print_color(f"Iter {iteration} (LLM Path): Generating candidate via LLM.", 'blue') - a_prime_params_dict = self._llm_generate_candidate() - - if a_prime_params_dict: - # Evaluate a_prime (from LLM path) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += a_prime_evals - total_samples += a_prime_evals - print_color(f"Iter {iteration} (LLM Path): New candidate a_prime (from LLM) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') - else: - print_color(f"Iter {iteration} (LLM Path): LLM failed to generate a valid candidate. Skipping addition to buffer.", 'red') - metrics['llm_generation_failures'] += 1 - continue - - # Common logic for adding a_prime to buffer - metrics['new_candidate_scores'].append(a_prime_score) - - if a_prime_params_dict and a_prime_score > -np.inf and a_prime_evals > 0: - new_candidate_entry = { - 'params': a_prime_params_dict, - 'score_sum': a_prime_score * a_prime_evals, - 'eval_count': a_prime_evals, - 'ucb_score': 0.0, - 'iteration_created': iteration - } - - if len(self.buffer) == self.max_buffer_size: - self._update_buffer_ucb_scores() - candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) - self.buffer.remove(candidate_to_evict) - evicted_mean_score = candidate_to_evict['score_sum'] / candidate_to_evict['eval_count'] if candidate_to_evict['eval_count'] > 0 else -np.inf - print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f}, MeanScore: {evicted_mean_score:.4f})", 'magenta') - - self.buffer.append(new_candidate_entry) - print_color(f"Iter {iteration}: Added new candidate (from {generation_method}) to buffer.", 'magenta') - elif a_prime_params_dict: - print_color(f"Iter {iteration}: New candidate a_prime (from {generation_method}) had invalid score/evals ({a_prime_score}, {a_prime_evals}), not added to buffer.", 'yellow') - - self._update_buffer_ucb_scores() - - # Logging - if self.buffer: - best_in_buffer = max(self.buffer, key=lambda c: (c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1))) - current_best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] if best_in_buffer['eval_count'] > 0 else 1) - metrics['best_candidate_scores'].append(current_best_score) - - valid_scores = [c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1) for c in self.buffer if c['eval_count'] > 0] - metrics['buffer_avg_score'].append(np.mean(valid_scores) if valid_scores else -np.inf) - metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) - else: - metrics['best_candidate_scores'].append(-np.inf) - metrics['buffer_avg_score'].append(-np.inf) - metrics['buffer_avg_evals'].append(0) - - if iteration % log_frequency == 0: - log_data = { - "iteration": iteration, - "best_score": metrics['best_candidate_scores'][-1], - "newly_evaluated_candidate_score": a_prime_score, - "buffer_size": len(self.buffer), - "buffer_avg_score": metrics['buffer_avg_score'][-1], - "buffer_avg_evals": metrics['buffer_avg_evals'][-1], - "total_evaluations_ucb_T": self._total_evaluations_tracker, - "total_samples": total_samples, - "generation_method_this_iter": generation_method, - "llm_generation_total_failures": metrics['llm_generation_failures'] - } - if generation_method == "ucb" and metrics['selected_action_ucb']: - log_data["selected_action_ucb"] = metrics['selected_action_ucb'][-1] - - print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Gen method: {generation_method}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') - - if save_frequency is not None and iteration % save_frequency == 0 and self.buffer: - best_overall_candidate_entry = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) - self.optimizer.update(best_overall_candidate_entry['params']) - if hasattr(self, 'save_agent'): - self.save_agent(save_path, iteration) - best_mean_score_for_save = best_overall_candidate_entry['score_sum'] / (best_overall_candidate_entry['eval_count'] if best_overall_candidate_entry['eval_count'] > 0 else 1E-9) - print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer (Mean Score: {best_mean_score_for_save:.4f}).", 'green') - else: - print_color(f"Iter {iteration}: save_agent method not found, skipping save.", 'yellow') - - print_color("UCB-LLM search finished.", 'blue') - if not self.buffer: - print_color("Buffer is empty at the end of search. No best candidate found.", 'red') - return metrics, -np.inf - - final_best_candidate = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) - final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] if final_best_candidate['eval_count'] > 0 else 1E-9) - final_best_evals = final_best_candidate['eval_count'] - print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_evals}", 'green') - - self.optimizer.update(final_best_candidate['params']) - - return metrics, float(final_best_score) - - def select(self, buffer): - '''Selects candidate with highest UCB score.''' - if not buffer: return None - return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) - - -class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): - """ - UCB Search Algorithm that uses LLM function approximation to select candidates. - """ - - def __init__(self, llm_model, *args, **kwargs): - super().__init__(*args, **kwargs) - self.llm_model = llm_model - self.llm = LLM(model=self.llm_model) - print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") - - def select(self, buffer): - """Generate a new candidate entry using LLM. Note: this doesn't add it to the buffer.""" - new_action_params = self._llm_generate_candidate() - new_candidate_entry = { - 'params': new_action_params, - 'score_sum': 0, - 'eval_count': 0, - 'ucb_score': 0.0, - 'iteration_created': 0 - } - return new_candidate_entry - - def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: - """ - Prompts an LLM with current buffer candidates to generate new string values for parameters. - Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. - """ - print_color("Attempting to generate candidate using LLM...", "blue") - if not self.buffer: - print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") - return None - - sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) - prompt_candidates = sorted_buffer - - serializable_candidate_summaries = [] - for cand_entry in prompt_candidates: - summary = { - "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, - "eval_count": cand_entry['eval_count'], - "ucb_score": round(cand_entry.get('ucb_score',0), 4), - } - serializable_candidate_summaries.append(summary) - - example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} - - prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} - ] - - print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - - llm_response = self.llm(messages=prompt_messages) - llm_response_str = llm_response.choices[0].message.content - - if not llm_response_str: - print_color("LLM returned an empty response.", "red") - return None - - # Clean the response string - cleaned_llm_response_str = llm_response_str.strip() - if cleaned_llm_response_str.startswith("```json"): - cleaned_llm_response_str = cleaned_llm_response_str[7:] - if cleaned_llm_response_str.endswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[:-3] - elif cleaned_llm_response_str.startswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[3:] - if cleaned_llm_response_str.endswith("```"): - cleaned_llm_response_str = cleaned_llm_response_str[:-3] - cleaned_llm_response_str = cleaned_llm_response_str.strip() - - if not cleaned_llm_response_str: - print_color("LLM response was empty after cleaning markdown/whitespace.", "red") - return None - - print_color(f"Cleaned LLM response: '{cleaned_llm_response_str}'", "magenta") - - # Fix common JSON formatting issues from LLM responses - try: - llm_params_raw = json.loads(cleaned_llm_response_str) - except json.JSONDecodeError as e: - print_color(f"Initial JSON parsing failed: {e}", "yellow") - print_color("Attempting to fix JSON formatting...", "yellow") - - fixed_json_str = smart_quote_replacement(cleaned_llm_response_str) - - try: - llm_params_raw = json.loads(fixed_json_str) - print_color("Successfully fixed JSON formatting", "green") - except json.JSONDecodeError as e2: - print_color(f"Smart quote replacement failed: {e2}", "yellow") - try: - simple_fixed = cleaned_llm_response_str.replace("'", '"') - llm_params_raw = json.loads(simple_fixed) - print_color("Fallback simple replacement succeeded", "green") - except json.JSONDecodeError as e3: - print_color(f"All JSON parsing attempts failed: {e3}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - if not isinstance(llm_params_raw, dict): - print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - candidate_params_dict = self.construct_update_dict(llm_params_raw) - return candidate_params_dict - - def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" - update_dict = {} - for node in self.agent.parameters(): - if node.trainable and node.py_name in suggestion: - try: - formatted_suggestion = suggestion[node.py_name] - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - if getattr(self, 'ignore_extraction_error', False): - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e - return update_dict - + return max(buffer, key=lambda c: c['ucb_score']) \ No newline at end of file From 37d5883da88106b4cd21836db5fdf1886a99ea0d Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 18 Jun 2025 15:17:46 -0700 Subject: [PATCH 035/172] add refactoring XML --- opto/optimizers/optoprime_v2.py | 96 ++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index f0c78258..60daf5fc 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -1,7 +1,16 @@ import json +from typing import Any, List, Dict, Union, Tuple from textwrap import dedent, indent -from opto.optimizers.optoprime import OptoPrime +from dataclasses import dataclass, asdict +from opto.optimizers.optoprime import OptoPrime, ProblemInstance +from opto.trace.nodes import ParameterNode, Node, MessageNode +from opto.trace.propagators import TraceGraph, GraphPropagator +from opto.trace.propagators.propagators import Propagator + +from opto.utils.llm import AbstractModel, LLM +from opto.optimizers.buffers import FIFOBuffer +import copy class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. @@ -22,7 +31,9 @@ class OptoPrimeV2(OptoPrime): In #Variables, #Inputs, #Outputs, and #Others, the format is: - = + + () = + If is (code), it means is the source code of a python code, which may include docstring and definitions. """ @@ -33,11 +44,12 @@ class OptoPrimeV2(OptoPrime): output_format_prompt = dedent( """ - Output_format: Your output should be in the following json format, satisfying the json syntax: - - {{ - "reasoning": , - "answer": , + Output_format: Your output should be in the following XML/HTML format: + + + Your reasoning + + "suggestion": {{ : , : , @@ -111,6 +123,76 @@ class OptoPrimeV2(OptoPrime): "documentation": "#Documentation", } + def __init__( + self, + parameters: List[ParameterNode], + llm: AbstractModel = None, + *args, + propagator: Propagator = None, + objective: Union[None, str] = None, + ignore_extraction_error: bool = True, # ignore the type conversion error when extracting updated values from LLM's suggestion + include_example=False, # TODO # include example problem and response in the prompt + memory_size=0, # Memory size to store the past feedback + max_tokens=4096, + log=True, + prompt_symbols=None, + **kwargs, + ): + super().__init__(parameters, *args, propagator=propagator, **kwargs) + self.ignore_extraction_error = ignore_extraction_error + self.llm = llm or LLM() + self.objective = objective or self.default_objective + self.example_problem = ProblemInstance.problem_template.format( + instruction=self.default_objective, + code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", + documentation="add: add x and y \nsubtract: subtract y from x", + variables="(int) a = 5", + constraints="a: a > 0", + outputs="(int) z = 1", + others="(int) y = 6", + inputs="(int) b = 1\n(int) c = 5", + feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", + stepsize=1, + ) + self.example_response = dedent( + """ + {"reasoning": 'In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.', + "suggestion": {"a": 10} + } + """ + ) + + self.include_example = include_example + self.max_tokens = max_tokens + self.log = [] if log else None + self.summary_log = [] if log else None + self.memory = FIFOBuffer(memory_size) + self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) + if prompt_symbols is not None: + self.prompt_symbols.update(prompt_symbols) + + @staticmethod + def repr_node_value(node_dict): + temp_list = [] + for k, v in node_dict.items(): + if "__code" not in k: + temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n") + else: + temp_list.append(f"\n(code) {k}:{v[0]}\n") + return "\n".join(temp_list) + + @staticmethod + def repr_node_constraint(node_dict): + temp_list = [] + for k, v in node_dict.items(): + if "__code" not in k: + if v[1] is not None: + temp_list.append(f"\n({type(v[0]).__name__}) {k}: {v[1]}\n") + else: + if v[1] is not None: + temp_list.append(f"\n(code) {k}: {v[1]}\n") + return "\n".join(temp_list) + def construct_prompt(self, summary, mask=None, *args, **kwargs): """Construct the system and user prompt.""" system_prompt = ( From d9b3bf04969e79d9c80fb9a4b88d0b48a6e27c69 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Wed, 18 Jun 2025 19:19:41 -0500 Subject: [PATCH 036/172] Modified the code based on the comments in PR page --- opto/trainer/algorithms/UCBsearch.py | 28 +++++++++++++------ .../algorithms/beamsearch_algorithm.py | 6 ++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 0a136eff..30277157 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -27,7 +27,8 @@ def __init__(self, agent: trace.Module, optimizer, max_buffer_size: int = 10, - ucb_exploration_factor: float = 1.0, + ucb_exploration_factor: float = 1.0, # Controls exploration vs exploitation tradeoff in UCB selection + # UCB formula: μ(a) + c * sqrt(ln(t) / n(a)), c is the exploration factor logger=None, num_threads: int = None, *args, @@ -36,6 +37,8 @@ def __init__(self, self.buffer = deque(maxlen=max_buffer_size) self.max_buffer_size = max_buffer_size + # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, + # lower values favor exploitation of well-performing candidates. self.ucb_exploration_factor = ucb_exploration_factor # To ensure optimizer_step can be called with bypassing=True if needed. @@ -76,7 +79,7 @@ def _evaluate_candidate(self, print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') return -np.inf, 0 - original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} self.optimizer.update(params_to_eval_dict) eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size @@ -114,6 +117,8 @@ def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations if total_tracked_evaluations == 0: # Should not happen if we init with one eval total_tracked_evaluations = 1 + # UCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation exploration_term = self.ucb_exploration_factor * \ math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) @@ -131,6 +136,7 @@ def train(self, guide, # Guide for train_dataset (feedback generation AND evaluation) train_dataset: Dict[str, List[Any]], *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset num_search_iterations: int = 100, train_batch_size: int = 2, evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations @@ -146,6 +152,10 @@ def train(self, """ Main training loop for UCB Search Algorithm. """ + # Default validation_dataset to train_dataset if not provided + if validation_dataset is None: + validation_dataset = train_dataset + num_threads = num_threads or self.num_threads log_frequency = log_frequency or eval_frequency self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate @@ -161,10 +171,10 @@ def train(self, } # 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. - print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, train_dataset, guide, evaluation_batch_size, num_threads # Use train_dataset and guide + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide ) self._total_evaluations_tracker += initial_evals total_samples += initial_evals @@ -173,7 +183,7 @@ def train(self, 'params': initial_params_dict, 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later 'eval_count': initial_evals, - 'ucb_score': 0.0, # Will be updated + 'ucb_score': None, # avoid accidental reads before it's initialized 'iteration_created': 0 } self.buffer.append(initial_candidate_entry) @@ -236,7 +246,7 @@ def train(self, if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} except Exception as e: print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') @@ -244,7 +254,7 @@ def train(self, # 4. Evaluate 'a_prime' on samples of validation set a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, train_dataset, guide, evaluation_batch_size, num_threads # Use train_dataset and guide + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide ) self._total_evaluations_tracker += a_prime_evals total_samples += evaluation_batch_size + train_batch_size @@ -263,7 +273,7 @@ def train(self, 'params': a_prime_params_dict, 'score_sum': a_prime_score * a_prime_evals, # Store sum 'eval_count': a_prime_evals, - 'ucb_score': 0.0, # Will be updated + 'ucb_score': None, # avoid accidental reads before it's initializad 'iteration_created': iteration } diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index 09a13578..a6eda61e 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -67,7 +67,7 @@ def train(self, print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') # Store original parameters to restore after each exploration - original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} # Dictionary to track metrics during beam search metrics = { @@ -384,7 +384,7 @@ def select(self, If return_scores is True: Tuple of (list of parameters, list of scores) """ # Store current parameters to restore later - current_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} # List to store (score, params) pairs scored_candidates = [] @@ -495,7 +495,7 @@ def train(self, print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') # Store original parameters - original_params = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} # Dictionary to track metrics metrics = { From a2a69c7b9d915aaaf59fcf9f2e78dc983c5cc440 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 18 Jun 2025 17:46:58 -0700 Subject: [PATCH 037/172] update signature, add test to check updating bundle functions --- opto/trace/modules.py | 10 +++++++--- tests/unit_tests/test_modules.py | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/opto/trace/modules.py b/opto/trace/modules.py index ee779100..bf33d6a3 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -7,6 +7,8 @@ from opto.trace.nodes import ParameterNode from opto.trace.projections import Projection, BlackCodeFormatter +import functools +from typing import List, Optional def model(cls): """ @@ -14,10 +16,13 @@ def model(cls): """ class ModelWrapper(cls, Module): - def model_dump(self, filename, projection: Projection = BlackCodeFormatter()): + def model_dump(self, filename, projections: Optional[List[Projection]] = None): """Dump the model's source code to a file, including all methods and attributes. Ignores dunder methods unless they were overridden by the user. """ + if projections is None: + projections = [BlackCodeFormatter()] + trace_model_body = f"class {cls.__name__}:\n" # Get all members of the class @@ -86,8 +91,7 @@ def replace_node(match): trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) - if projection is not None: - trace_model_body = projection.project(trace_model_body) + trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) with open(filename, "w") as f: f.write(trace_model_body) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 46971917..5494a5ce 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -315,6 +315,8 @@ def non_trainable_method(self, y): obj._trainable._data = 100 obj._non_trainable._data = 200 + obj.trainable_method.parameter._data = "def trainable_method(self, x):\n return x + 3" + temp_file = "temp_mixed.py" try: obj.model_dump(temp_file) @@ -331,10 +333,12 @@ def non_trainable_method(self, y): assert "@bundle" not in content # Check if methods are present but without decorators assert "def trainable_method" in content + assert "return x + 3" in content assert "def non_trainable_method" in content # Check if regular attribute is present assert "regular_attr" in content finally: if os.path.exists(temp_file): - os.remove(temp_file) + pass + # os.remove(temp_file) From cd01e9ca5e94e79cafdca8d1edeb6efea53e4d9d Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 18 Jun 2025 17:50:24 -0700 Subject: [PATCH 038/172] add the import test --- tests/unit_tests/test_modules.py | 62 ++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 5494a5ce..1934ae5b 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -339,6 +339,64 @@ def non_trainable_method(self, y): assert "regular_attr" in content finally: if os.path.exists(temp_file): - pass - # os.remove(temp_file) + os.remove(temp_file) +def test_model_dump_and_import(): + @model + class StrangeCalculator: + def __init__(self): + super().__init__() + self.offset = node(2, trainable=True) + self.multiplier = node(1.5, trainable=True) + + @bundle(trainable=True) + def add(self, x, y): + """Add two numbers with an offset""" + return x + y + self.offset + + @bundle(trainable=True) + def multiply(self, x, y): + """Multiply two numbers with a multiplier""" + return x * y * self.multiplier + + # Create instance and modify parameters + calc = StrangeCalculator() + calc.offset._data = 3 + calc.multiplier._data = 2.0 + calc.add.parameter._data = "def add(self, x, y):\n return x + y + self.offset + 1" + calc.multiply.parameter._data = "def multiply(self, x, y):\n return x * y * self.multiplier * 2" + + # Dump the model + temp_file = "temp_calculator.py" + try: + calc.model_dump(temp_file) + + # Import the dumped class + import importlib.util + spec = importlib.util.spec_from_file_location("temp_calculator", temp_file) + temp_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(temp_module) + + # Get the imported class + ImportedCalculator = temp_module.StrangeCalculator + + # Create instance and test functionality + imported_calc = ImportedCalculator() + + # Test the modified behavior + result_add = imported_calc.add(5, 3) + result_multiply = imported_calc.multiply(4, 2) + + # Verify the results match our expected modified behavior + # add: 5 + 3 + 3 + 1 = 12 + # multiply: 4 * 2 * 2.0 * 2 = 32 + assert result_add == 12, f"Expected 12, got {result_add}" + assert result_multiply == 32, f"Expected 32, got {result_multiply}" + + # Verify the attributes have the correct values + assert imported_calc.offset == 3 + assert imported_calc.multiplier == 2.0 + + finally: + if os.path.exists(temp_file): + os.remove(temp_file) From d996ac711e2467d21bdfa4f3899d6027039bfd61 Mon Sep 17 00:00:00 2001 From: adith387 Date: Thu, 19 Jun 2025 17:10:06 -0700 Subject: [PATCH 039/172] Update README.md with link to roadmap --- README.md | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f90f7084..e5e94e9a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ losses, natural language text, compiler errors, etc.). Trace generalizes the bac propagating an AI system's execution trace. Trace is implemented as a PyTorch-like Python library. Users write Python code directly and can use Trace primitives to optimize certain parts, just like training neural networks! -[Paper](https://arxiv.org/abs/2406.16218) | [Project website](https://microsoft.github.io/Trace/) | [Documentation](https://microsoft.github.io/Trace/intro.html) | [Blogpost](https://www.microsoft.com/en-us/research/blog/tracing-the-path-to-self-adapting-ai-agents/) | [Discord channel](https://discord.gg/4VeAvwFcWy) +[Paper](https://arxiv.org/abs/2406.16218) | [Project website](https://microsoft.github.io/Trace/) | [Documentation](https://microsoft.github.io/Trace/intro.html) | [Blogpost](https://www.microsoft.com/en-us/research/blog/tracing-the-path-to-self-adapting-ai-agents/) | [Discord channel](https://discord.gg/4VeAvwFcWy) | [Roadmap](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing)

drawing @@ -38,6 +38,7 @@ git is unable to clone the repository. ## Updates +- **2025.5.9** Adith Swaminathan gave a talk at Netflix Workshop on Personalization, Recommendation and Search (PRS)[https://prs2025.splashthat.com/] - **2025.5.1** Ching-An Cheng gave a talk at 2nd Texas Colloquium on Distributed Learning (TL;DR)[https://sites.google.com/view/tldr-2025] - **2025.2.7** Trace was featured in the [G-Research NeurIPS highlight](https://www.gresearch.com/news/neurips-paper-reviews-2024-8/) by the Science Director Hugh Salimbeni. - **2024.12.10** Trace was demoed in person at NeurIPS 2024 Expo. @@ -391,6 +392,26 @@ Explains the role of feedback in LLM-based optimizers. An early work that influe +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Roadmap + +View our [Public Roadmap](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) + +You can learn about the features we are working on, areas where you can contribute, and future plans for Trace. + ## Evaluation A previous version of Trace was tested with gpt-4-0125-preview on numerical optimization, simulated traffic control, @@ -419,20 +440,6 @@ see [example](https://community.openai.com/t/gpt-4o-doesnt-consistently-respect- - The system should not be used in highly regulated domains where inaccurate outputs could suggest actions that lead to injury or negatively impact an individual's legal, financial, or life opportunities. -## Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. - -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft From b8f6137fb59a1759929f656b71563801b9626b9b Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Fri, 20 Jun 2025 15:40:07 -0500 Subject: [PATCH 040/172] Fix the bug by using optimizer.parameters --- opto/trainer/algorithms/UCBsearch.py | 6 +++--- opto/trainer/algorithms/beamsearch_algorithm.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 30277157..55114772 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -79,7 +79,7 @@ def _evaluate_candidate(self, print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') return -np.inf, 0 - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} self.optimizer.update(params_to_eval_dict) eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size @@ -172,7 +172,7 @@ def train(self, # 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} initial_score, initial_evals = self._evaluate_candidate( initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide ) @@ -246,7 +246,7 @@ def train(self, if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} except Exception as e: print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index a6eda61e..7f0fb423 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -67,7 +67,7 @@ def train(self, print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') # Store original parameters to restore after each exploration - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} # Dictionary to track metrics during beam search metrics = { @@ -384,7 +384,7 @@ def select(self, If return_scores is True: Tuple of (list of parameters, list of scores) """ # Store current parameters to restore later - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} # List to store (score, params) pairs scored_candidates = [] @@ -495,7 +495,7 @@ def train(self, print_color(f"Using validation_dataset_size={validation_dataset_size} for intermediate evaluations", 'blue') # Store original parameters - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters()} + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} # Dictionary to track metrics metrics = { From 1a890242fc172c794ff422b356195aacd54cd377 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Fri, 20 Jun 2025 18:36:42 -0500 Subject: [PATCH 041/172] Add logging for initial, validation, and test scores in Beamsearch algorithms --- .../algorithms/beamsearch_algorithm.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index 7f0fb423..0451455e 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -92,6 +92,9 @@ def train(self, initial_test_score = np.mean(initial_test_scores) if all([s is not None for s in initial_test_scores]) else -np.inf print_color(f"Initial test score: {initial_test_score:.4f}", 'yellow') + # Log initial test score + self.logger.log('Initial test score', initial_test_score, 0, color='blue') + # Add initial score to metrics for logging metrics['test_scores'].append(initial_test_score) metrics['test_depths'].append(1) # Represent initial score at depth 0 @@ -159,6 +162,13 @@ def train(self, print_color(f"Depth {depth+1} - Best validation score: {best_score:.4f}", 'green') + # Log validation metrics + step_num = depth + 1 + self.logger.log('Best validation score', best_score, step_num, color='green') + self.logger.log('Average validation score', np.mean(scores), step_num, color='cyan') + self.logger.log('Min validation score', min(scores), step_num, color='yellow') + self.logger.log('Max validation score', max(scores), step_num, color='magenta') + # Evaluate on test set every test_frequency steps if test_dataset is not None and ((depth + 1) % test_frequency == 0): # Update agent with best parameters from this depth @@ -192,6 +202,9 @@ def train(self, metrics['test_depths'].append(depth + 1) print_color(f"Depth {depth+1} - Test score: {test_score:.4f}", 'magenta') + + # Log test score + self.logger.log('Periodic test score', test_score, step_num, color='magenta') # Final selection - choose the best beam using FULL validation set print_color("\n===== Final Selection Using Full Validation Set =====", 'blue') @@ -217,6 +230,10 @@ def train(self, best_params = best_beams[0] final_validation_score = final_val_scores[0] if final_val_scores else -np.inf + # Log final validation score + final_step = max_depth + 1 + self.logger.log('Final validation score', final_validation_score, final_step, color='blue') + # Apply the best parameters self.optimizer.update(best_params) @@ -259,6 +276,9 @@ def train(self, if final_test_score is not None: print_color(f"BEST BEAM - Test score: {final_test_score:.4f}", 'green') + # Log final test score + self.logger.log('Final test score', final_test_score, final_step, color='green') + # Save the best model if save_frequency is not None and save_frequency > 0: self.save_agent(save_path, 0) @@ -517,6 +537,10 @@ def train(self, ) initial_test_score = np.mean(initial_test_scores) if all([s is not None for s in initial_test_scores]) else -np.inf print_color(f"Initial test score: {initial_test_score:.4f}", 'yellow') + + # Log initial test score + self.logger.log('Initial test score', initial_test_score, 0, color='blue') + metrics['test_scores'].append(initial_test_score) metrics['test_depths'].append(1) # Start depth at 1 for consistency @@ -574,6 +598,14 @@ def train(self, metrics['depth_scores'].append(scores) print_color(f"Depth {depth+1} - Best validation score: {best_score_this_depth:.4f}", 'green') + # Log validation metrics + step_num = depth + 1 + self.logger.log('Best validation score', best_score_this_depth, step_num, color='green') + self.logger.log('Average validation score', np.mean(scores), step_num, color='cyan') + self.logger.log('Min validation score', min(scores), step_num, color='yellow') + self.logger.log('Max validation score', max(scores), step_num, color='magenta') + self.logger.log('History buffer size', len(self.parameter_history), step_num, color='orange') + best_idx = scores.index(best_score_this_depth) # Find index of best score best_params = beams[best_idx] # Get corresponding params @@ -609,6 +641,9 @@ def train(self, metrics['test_scores'].append(test_score) metrics['test_depths'].append(depth + 1) print_color(f"Depth {depth+1} - Test score: {test_score:.4f}", 'magenta') + + # Log test score + self.logger.log('Periodic test score', test_score, step_num, color='magenta') # >>> End Main Loop <<< @@ -624,7 +659,11 @@ def train(self, final_validation_score = final_val_scores[0] if final_val_scores else -np.inf best_params = best_beams[0] if best_beams else original_params # Fallback to original if empty - # Apply best parameters + # Log final validation score + final_step = max_depth + 1 + self.logger.log('Final validation score', final_validation_score, final_step, color='blue') + + # Apply the best parameters self.optimizer.update(best_params) # Print final parameters @@ -641,6 +680,9 @@ def train(self, final_test_score = np.mean(final_test_scores_eval) if all([s is not None for s in final_test_scores_eval]) else -np.inf print_color(f"BEST BEAM - Test score: {final_test_score:.4f}", 'green') + # Log final test score + self.logger.log('Final test score', final_test_score, final_step, color='green') + # Save agent if configured if kwargs.get('save_frequency', None) is not None and kwargs['save_frequency'] > 0: self.save_agent(kwargs.get('save_path', "checkpoints/agent.pkl"), 0) From 8035c24aabef4d02a78e5d7f7792c7d0f5be2f75 Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Fri, 20 Jun 2025 18:48:10 -0500 Subject: [PATCH 042/172] Add detailed logging for UCB search algorithm --- opto/trainer/algorithms/UCBsearch.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 55114772..9ff6f61b 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -179,6 +179,10 @@ def train(self, self._total_evaluations_tracker += initial_evals total_samples += initial_evals + # Log initial evaluation + self.logger.log('Initial UCB score', initial_score, 0, color='blue') + self.logger.log('Initial evaluations', initial_evals, 0, color='cyan') + initial_candidate_entry = { 'params': initial_params_dict, 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later @@ -200,6 +204,9 @@ def train(self, self._update_buffer_ucb_scores() # Ensure UCB scores are fresh action_candidate_a = self.select(self.buffer) + # Log selected action UCB score + self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') + self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') @@ -259,6 +266,11 @@ def train(self, self._total_evaluations_tracker += a_prime_evals total_samples += evaluation_batch_size + train_batch_size metrics['new_candidate_scores'].append(a_prime_score) + + # Log new candidate performance + self.logger.log('New candidate score', a_prime_score, iteration, color='green') + self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') # 5. Update the stats of 'a' (action_candidate_a) based on the training batch experience @@ -310,6 +322,15 @@ def train(self, "total_evaluations_tracker": self._total_evaluations_tracker, "total_samples": total_samples # Add new metric } + + # Log all important metrics + self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') + self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') + self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') + self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') + self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') + self.logger.log('Total samples processed', log_data['total_samples'], iteration, color='yellow') + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') # Save agent (e.g., the one with highest mean score in buffer) @@ -321,13 +342,26 @@ def train(self, # End of search loop print_color("UCB search finished.", 'blue') + + # Log final training summary + final_iteration = num_search_iterations + self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') + self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') + if not self.buffer: print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') return metrics, -np.inf # Select the best candidate based on highest mean score (exploitation) final_best_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + + # Log final results + self.logger.log('Final best score', final_best_score, final_iteration, color='green') + self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') + self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') # Load best parameters into the agent From 249eba4249ee17c0721eeaaea00ee685935de232 Mon Sep 17 00:00:00 2001 From: Xavier Daull Date: Sat, 21 Jun 2025 17:14:01 +0200 Subject: [PATCH 043/172] refined optoprimmulti for more stability --- opto/optimizers/optoprimemulti.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py index 19dadb70..be7cfa30 100644 --- a/opto/optimizers/optoprimemulti.py +++ b/opto/optimizers/optoprimemulti.py @@ -2,10 +2,9 @@ import json from typing import List, Dict - - from opto.trace.propagators import GraphPropagator from opto.optimizers.optoprime import OptoPrime +from opto.utils.llm import LLMFactory from concurrent.futures import ThreadPoolExecutor, as_completed @@ -69,6 +68,8 @@ def _get_llms_for_generation(self, num_responses: int): llm = self._get_llm_for_profile(profile) llms.append(llm) + return llms + def call_llm( self, system_prompt: str, @@ -209,11 +210,10 @@ def generate_candidates( if self.llm_profiles is not None and len(self.llm_profiles) > 0 and generation_technique == "multi_llm": llms = self._get_llms_for_generation(num_responses) - temperatures = [temp_max - i * (temp_max - temp_min) / max(1, num_responses - 1) for i in range(num_responses)] # Prepare arguments for parallel execution arg_dicts = [] - for i, (llm, temp) in enumerate(zip(llms, temperatures)): + for i, llm in enumerate(llms): profile_name = self.llm_profiles[i % len(self.llm_profiles)] if self.llm_profiles else "default" modified_system_prompt = f"{system_prompt}\n\n[Using {profile_name} model for diverse perspective]" @@ -223,7 +223,7 @@ def generate_candidates( verbose=verbose, max_tokens=max_tokens, num_responses=1, - temperature=temp, + temperature=temp_min, llm=llm # Use specific LLM )) @@ -251,7 +251,7 @@ def generate_candidates( verbose=verbose, max_tokens=max_tokens, num_responses=1, - temperature=0.0, + temperature=temp_min, ) if response and len(response) > 0: @@ -267,7 +267,7 @@ def generate_candidates( f"CANDIDATE {idx + 1}: <<<\n{cand}\n>>>" for idx, cand in enumerate(candidates) ) - meta_prompt = f"{system_prompt}\nGiven the following candidate solutions, propose a new alternative optimal solution to user's prompt using their same JSON format (suggest only trainable codes/variables to modify, never inputs):\n{previous_solutions}\n" + meta_prompt = f"{system_prompt}\nGiven the following prior CANDIDATE solutions, answer with a very different new CANDIDATE optimal solution to user's prompt using their same JSON format (suggest only trainable codes/variables to modify, never inputs):\n{previous_solutions}\n" response = self.call_llm( system_prompt=meta_prompt, @@ -275,7 +275,7 @@ def generate_candidates( verbose=verbose, max_tokens=max_tokens, num_responses=1, - temperature=0.0, + temperature=temp_min, ) if response and len(response) > 0: From 62f5fdaaabe6a85ff7f8b98ff4605a7b652db267 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 24 Jun 2025 18:31:42 +0000 Subject: [PATCH 044/172] Update CONTRIBUTING.md and add opto/features. --- CONTRIBUTING.md | 65 +++++++++++++++++++++++++++++++++++++++ opto/features/__init__.py | 0 2 files changed, 65 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 opto/features/__init__.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cdea3af6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contribution Guideline + +Trace is an actively growing project and under active maintenance and development! We maintain two major branches `main` and `experimental`. The `main` branch is the most stable, version-controlled branch and it is what the PyPI package is linked to. On the other hand, the `experimental` branch is the dev branch, which will change more dynamically in in preparation for the next version update. + +### Review Process and Update Dynamics + +Contribution to these two branches requires going through a review process via PR and passing all unit tests in CI. +Merging a PR requires at least one reviewer different from the contributor, except for those marked as [**LIGHT**] below. + +Here is an outline: + +1. `main` will be regularly updated by PRs based on the development of the `experimental` branch following the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). Each update will result in a version update of the first two digits. + +2. Except for the planned roadmap, `main` will only be updated to fix bugs. Bug fix to what is in `main` should be submitted as PR to `main`. This will trigger a quicker review and result in a version update in the third digit, and the `experimental` branch will then rebase on the updated `main`. + +3. For feature development, PR should be submitted to the `experimental` branch without version update. Generally, the `experimental` branch aims to realize the milestones listed in the next version update in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). If applicable, new determinstic unit tests should be added under `tests/unit_tests`. Otherwise, an example run script should be added in `examples`. + +4. [**LIGHT**] Bugs fix to the new changes introduced in the `experimental` branch should be submitted as a PR to the `experimental` branch. This PR will be incoporated quickly with a light review. + +5. [**LIGHT**] For contributions under the directory `opto/features`, they should be submitted as PR to the `experimental` branch. These usually are not under roadmap and are content not made as dependable by codes in other directories. That is, contents under `opto/features/A` should not be imported by files other than those under `opto/features/A`. So long as this rule is met, the PR will be incorprated under a light review. + +6. [Exception] Updates to non-coding elements (like documents) does not necessarily require a PR. + +The above is applicable to all contributors including the maintainers. + + +### Communication + +1. Quick questions should be posted on Discord channel. + +2. For bugs, feature requests, contributions, or questions that might be related to a broader audience, post them as issues on the github page. + + +# Steps for Contributions + +We welcome your contributions and involvement. Below are instructions for how to contribute to Trace. + +## Quick Bug Fix + +If there is a minor, isolated bug that can be directly fixed, please report it as an issue or submit a PR to be merged into the `main` branch or `experimental` branch, depending on where the issue arises. + + +## Contributing Feature + +We welcome new ideas. + +### Step 1: Feature Spec Doc +A feature should first be written as a Google Doc (an example is [here](https://docs.google.com/document/d/1FX1ygc8lgFpFn3ni3E2A_DCGtn505PpAM8QaAjEovsA/edit?usp=sharing)). + +### Step 2: Create an Issue +An issue should be created, and under the issue, the doc is linked. People should be allowed to comment on the doc. + +### Step 3: Implement Feature +Create a separate branch, extending from the `experimental` branch. This branch contains all the new features that have not been merged into the `main` branch yet. +Make sure your features are implemented, along with `unit tests` or `examples` to show how it's used. + +### Step 4: Create a Pull Request +Create a PR formally to merge into the experiment branch and request a review. For standalone features, put the changes under `opto/features/`. This will trigger the lightest review that only checks for malicious code, or if the feature does not pass its own unit tests. +For changes to the rest, expect a slightly longer review process as we work out how the changes should be integrated with the core library. + + +### Step 5: Merge into Experimental +Once the request is approved, it will be merged into the `experimental` branch. + + diff --git a/opto/features/__init__.py b/opto/features/__init__.py new file mode 100644 index 00000000..e69de29b From 8431f6d9be160e10f8c5efd519937b0fbe1a7893 Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Tue, 24 Jun 2025 11:49:17 -0700 Subject: [PATCH 045/172] Add files via upload --- docs/images/contributing_workflow.png | Bin 0 -> 39433 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/contributing_workflow.png diff --git a/docs/images/contributing_workflow.png b/docs/images/contributing_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..badf28a3b6758fdfb08338466bfb8a9b7c4955eb GIT binary patch literal 39433 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 literal 0 HcmV?d00001 From 9184307c92d10c9a85f812d3f5115be542649b8b Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Tue, 24 Jun 2025 11:58:40 -0700 Subject: [PATCH 046/172] Update CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdea3af6..853d3ce6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,7 @@ Here is an outline: The above is applicable to all contributors including the maintainers. +![workflow](https://github.com/AgentOpt/Trace/blob/experimental/docs/images/contributing_workflow.png?raw=true) ### Communication From 06daabd1b8f8eb46feac63d4372a06e367a59ec1 Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Tue, 24 Jun 2025 12:01:10 -0700 Subject: [PATCH 047/172] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 853d3ce6..c0d97cce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,9 @@ Here is an outline: 6. [Exception] Updates to non-coding elements (like documents) does not necessarily require a PR. -The above is applicable to all contributors including the maintainers. +The above is applicable to all contributors, including the maintainers. + +All the features and bug fixes are merged into the experimental branch. After features are all added to the experimental branch, a version branch (e.g., `0.2.1`) will be created from `experimental`, and it will be staged for a release (merge into the main branch). ![workflow](https://github.com/AgentOpt/Trace/blob/experimental/docs/images/contributing_workflow.png?raw=true) From 9405815dd328981d0c2b43d1e11566fd7baaf57e Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 24 Jun 2025 19:29:47 +0000 Subject: [PATCH 048/172] Add example usages of LLMFactory in docstring. --- opto/utils/llm.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index 5039b266..9f419c26 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -240,7 +240,42 @@ def create(self, **config: Any): } class LLMFactory: - """Factory for creating LLM instances with predefined profiles.""" + """Factory for creating LLM instances with predefined profiles. + + The code comes with these built-in profiles: + + llm_default = LLM(profile="default") # gpt-4o-mini + llm_premium = LLM(profile="premium") # gpt-4 + llm_cheap = LLM(profile="cheap") # gpt-4o-mini + llm_fast = LLM(profile="fast") # gpt-3.5-turbo-mini + llm_reasoning = LLM(profile="reasoning") # o1-mini + + You can override those built-in profiles: + + LLMFactory.register_profile("default", "LiteLLM", model="gpt-4o", temperature=0.5) + LLMFactory.register_profile("premium", "LiteLLM", model="o1-preview", max_tokens=8000) + LLMFactory.register_profile("cheap", "LiteLLM", model="gpt-3.5-turbo", temperature=0.9) + LLMFactory.register_profile("fast", "LiteLLM", model="gpt-3.5-turbo", max_tokens=500) + LLMFactory.register_profile("reasoning", "LiteLLM", model="o1-preview") + + An Example of using Different Backends + + # Register custom profiles for different use cases + LLMFactory.register_profile("advanced_reasoning", "LiteLLM", model="o1-preview", max_tokens=4000) + LLMFactory.register_profile("claude_sonnet", "LiteLLM", model="claude-3-5-sonnet-latest", temperature=0.3) + LLMFactory.register_profile("custom_server", "CustomLLM", model="llama-3.1-8b") + + # Use in different contexts + reasoning_llm = LLM(profile="advanced_reasoning") # For complex reasoning + claude_llm = LLM(profile="claude_sonnet") # For Claude responses + local_llm = LLM(profile="custom_server") # For local deployment + + # Single LLM optimizer with custom profile + optimizer1 = OptoPrime(parameters, llm=LLM(profile="advanced_reasoning")) + + # Multi-LLM optimizer with multiple profiles + optimizer2 = OptoPrimeMulti(parameters, llm_profiles=["cheap", "premium", "claude_sonnet"], generation_technique="multi_llm") + """ # Default profiles for different use cases _profiles = { From 668d8b9124f2dc9c4b26c1ec5da9001d8e70dea9 Mon Sep 17 00:00:00 2001 From: Allen Nie Date: Tue, 24 Jun 2025 13:52:22 -0700 Subject: [PATCH 049/172] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0d97cce..60446573 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ Here is an outline: 5. [**LIGHT**] For contributions under the directory `opto/features`, they should be submitted as PR to the `experimental` branch. These usually are not under roadmap and are content not made as dependable by codes in other directories. That is, contents under `opto/features/A` should not be imported by files other than those under `opto/features/A`. So long as this rule is met, the PR will be incorprated under a light review. -6. [Exception] Updates to non-coding elements (like documents) does not necessarily require a PR. +6. [Exception] Core contributors only: Updates to non-coding elements (like documents) do not necessarily require a PR The above is applicable to all contributors, including the maintainers. From 05f9bdf0cdb97d473e4c9eecc28d09b228a428a5 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 24 Jun 2025 23:55:42 -0700 Subject: [PATCH 050/172] Fix API inconsistency of update in Minibatch, which results in num_threads not set correctly for BasicSearchAlgorithm. --- opto/trainer/algorithms/basic_algorithms.py | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 9baa09e5..4bc8247d 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -145,7 +145,7 @@ def train(self, outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs, infos) ] # Update the agent - score = self.update(outputs, verbose=verbose) + score = self.update(outputs, verbose=verbose, num_threads=num_threads, **kwargs) # Reject the update if the score on the current batch is not improved if ensure_improvement: @@ -225,14 +225,16 @@ def forward(self, agent, x, guide, info): """ raise NotImplementedError("Subclasses must implement this method") - def update(self, outputs, verbose=False): + def update(self, outputs, verbose=False, num_threads=None, **kwargs): """ Subclasses can implement this method to update the agent. Args: outputs: returned value from self.step verbose: whether to print the output of the agent + num_threads: maximum number of threads to use (overrides self.num_threads) Returns: score: average score of the minibatch of inputs """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads raise NotImplementedError("Subclasses must implement this method") @@ -254,15 +256,18 @@ class MinibatchAlgorithm(Minibatch): def forward(self, agent, x, guide, info): return standard_optimization_step(agent, x, guide, info) # (score, target, feedback) - def update(self, outputs, *args, **kwargs): + def update(self, outputs, verbose=False, num_threads=None, **kwargs): """ Subclasses can implement this method to update the agent. Args: outputs: returned value from self.step verbose: whether to print the output of the agent + num_threads: maximum number of threads to use (overrides self.num_threads) Returns: score: average score of the minibatch of inputs """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + scores, targets, feedbacks = [], [], [] # Concatenate the targets and feedbacks into a single string for target, score, feedback in outputs: @@ -276,14 +281,14 @@ def update(self, outputs, *args, **kwargs): # Update the agent using the feedback self.optimizer.zero_feedback() self.optimizer.backward(target, feedback) - self.optimizer_step(*args, **kwargs) # update the agent + self.optimizer_step(verbose=verbose, num_threads=num_threads, **kwargs) # update the agent return average_score # return the average score of the minibatch of inputs - def optimizer_step(self, bypassing=False, *args, **kwargs): + def optimizer_step(self, bypassing=False, verbose=False, num_threads=None, **kwargs): """ Subclasses can implement this method to update the agent. """ # We separate this method from the update method to allow subclasses to implement their own optimization step. - return self.optimizer.step(*args, bypassing=bypassing, **kwargs) + return self.optimizer.step(bypassing=bypassing, verbose=verbose, **kwargs) class BasicSearchAlgorithm(MinibatchAlgorithm): @@ -318,9 +323,11 @@ def train(self, min_score=min_score, verbose=verbose, num_threads=num_threads, **kwargs) # This code should be reusable for other algorithms - def optimizer_step(self, bypassing=False, verbose=False, *args, **kwargs): + def optimizer_step(self, bypassing=False, verbose=False, num_threads=None, **kwargs): """ Use the optimizer to propose multiple updates and select the best one based on validation score. """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + def validate(): """ Validate the agent on the validation dataset. """ scores = evaluate(self.agent, @@ -328,18 +335,19 @@ def validate(): self.validate_dataset['inputs'], self.validate_dataset['infos'], min_score=self.min_score, - num_threads=self.num_threads, + num_threads=num_threads, description="Validating proposals") return np.mean(scores) if all([s is not None for s in scores]) else -np.inf # TODO perhaps we can ask for multiple updates in one query or use different temperatures in different queries # Generate different proposals step_kwargs = dict(bypassing=True, verbose='output') # we don't print the inner full message + step_kwargs.update(kwargs) # update with additional kwargs if provided use_asyncio = self._use_asyncio() if use_asyncio: update_dicts = async_run([super().optimizer_step]*self.num_proposals, kwargs_list=[step_kwargs] * self.num_proposals, - max_workers=self.num_threads, + max_workers=num_threads, description=f"Generating {self.num_proposals} proposals") # async step else: update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(self.num_proposals)] From 3a7043e518b071ab30eee4502ca41c1549a1f4b0 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 00:01:33 -0700 Subject: [PATCH 051/172] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60446573..cf1771c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ Merging a PR requires at least one reviewer different from the contributor, exce Here is an outline: -1. `main` will be regularly updated by PRs based on the development of the `experimental` branch following the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). Each update will result in a version update of the first two digits. +1. `main` will be regularly updated by PRs based on the development of the `experimental` branch following the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). Each update will result in a version update. 2. Except for the planned roadmap, `main` will only be updated to fix bugs. Bug fix to what is in `main` should be submitted as PR to `main`. This will trigger a quicker review and result in a version update in the third digit, and the `experimental` branch will then rebase on the updated `main`. From ea36515b7ffc4c9575a3d9539b67d396ddaac7cb Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 00:08:03 -0700 Subject: [PATCH 052/172] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf1771c7..8ba29bb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,9 +21,9 @@ Here is an outline: 6. [Exception] Core contributors only: Updates to non-coding elements (like documents) do not necessarily require a PR -The above is applicable to all contributors, including the maintainers. +The above is applicable to all contributors, including the core contributors and maintainers. -All the features and bug fixes are merged into the experimental branch. After features are all added to the experimental branch, a version branch (e.g., `0.2.1`) will be created from `experimental`, and it will be staged for a release (merge into the main branch). +In a regular development cycle, all features and bug fixes are merged into the experimental branch. After the items listed in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) are all added to the `experimental` branch, a version branch (e.g., `0.2.1`) will be created from `experimental`, and it will be staged for a release (to be merged into the `main` branch with a PR). At this point, the version number of the `experimental` branch will be updated to start the development of the next version. ![workflow](https://github.com/AgentOpt/Trace/blob/experimental/docs/images/contributing_workflow.png?raw=true) From 003c33d8803a5d700b7e995b7eff0a9a8c02a8a9 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 00:08:35 -0700 Subject: [PATCH 053/172] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ba29bb4..eff59005 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Trace is an actively growing project and under active maintenance and development! We maintain two major branches `main` and `experimental`. The `main` branch is the most stable, version-controlled branch and it is what the PyPI package is linked to. On the other hand, the `experimental` branch is the dev branch, which will change more dynamically in in preparation for the next version update. -### Review Process and Update Dynamics +### Development and Review Process Contribution to these two branches requires going through a review process via PR and passing all unit tests in CI. Merging a PR requires at least one reviewer different from the contributor, except for those marked as [**LIGHT**] below. From 2285916f182a0122858a801147e1a295fb4e6ba6 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 25 Jun 2025 22:03:43 +0000 Subject: [PATCH 054/172] Remove constraint in Node classes --- .../evals/textgrad_prompt_optimization.py | 2 +- opto/optimizers/optoprime.py | 8 ++--- opto/optimizers/textgrad.py | 1 - opto/trace/bundle.py | 2 +- opto/trace/nodes.py | 34 +++++++------------ 5 files changed, 18 insertions(+), 29 deletions(-) diff --git a/examples/textgrad_examples/evals/textgrad_prompt_optimization.py b/examples/textgrad_examples/evals/textgrad_prompt_optimization.py index dc6111f0..e87b3b9e 100644 --- a/examples/textgrad_examples/evals/textgrad_prompt_optimization.py +++ b/examples/textgrad_examples/evals/textgrad_prompt_optimization.py @@ -102,7 +102,7 @@ def run_validation_revert(system_prompt: tg.Variable, results, model, eval_fn, v # Testing the 0-shot performance of the evaluation engine system_prompt = trace.node(STARTING_SYSTEM_PROMPT, trainable=True, - constraint="structured system prompt to a somewhat capable language model that specifies the behavior and strategies for the QA task") + description="structured system prompt to a somewhat capable language model that specifies the behavior and strategies for the QA task") # model_evaluation = tg.BlackboxLLM(llm_api_eval, system_prompt) def model_evaluation(x): diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 5a5c5c36..74e5d1f4 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -42,7 +42,7 @@ def node_to_function_feedback(node_feedback: TraceGraph): visited.add(node) if node.is_root: # Need an or condition here - roots.update({node.py_name: (node.data, node._constraint)}) + roots.update({node.py_name: (node.data, node.description)}) else: # Some might be root (i.e. blanket nodes) and some might be intermediate nodes # Blanket nodes belong to roots @@ -52,12 +52,12 @@ def node_to_function_feedback(node_feedback: TraceGraph): documentation.update({get_fun_name(node): node.description}) graph.append((level, repr_function_call(node))) if level == depth: - output.update({node.py_name: (node.data, node._constraint)}) + output.update({node.py_name: (node.data, node.description)}) else: - others.update({node.py_name: (node.data, node._constraint)}) + others.update({node.py_name: (node.data, node.description)}) else: # this is a blanket node (classified into roots) - roots.update({node.py_name: (node.data, node._constraint)}) + roots.update({node.py_name: (node.data, node.description)}) return FunctionFeedback( graph=graph, diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index f01d382a..722df049 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -413,7 +413,6 @@ def _update_prompt(self, node: Node, gradients: List[GradientInfo]): "variable_value": node.data, "variable_grad": self._get_gradient_and_context_text(gradients), "variable_short": get_short_value(node.data), - "constraint_text": node._constraint, "new_variable_start_tag": self.new_variable_tags[0], "new_variable_end_tag": self.new_variable_tags[1], # "in_context_examples": "\n".join(self.in_context_examples), diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index db51f8eb..0e1c38ff 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -191,7 +191,7 @@ def __init__( self.parameter = ParameterNode( self.info["source"], name="__code", - constraint="The code should start with:\n" + signature, + description="The code should start with:\n" + signature, projections=projections, ) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index d97a65e6..8186175a 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -8,7 +8,7 @@ import contextvars -def node(data, name=None, trainable=False, description=None, constraint=None): +def node(data, name=None, trainable=False, description=None): """Create a Node object from data. Args: @@ -16,7 +16,6 @@ def node(data, name=None, trainable=False, description=None, constraint=None): name (str, optional): The name of the Node. trainable (bool, optional): Whether the Node is trainable. Defaults to False. description (str, optional): A string describing the data. - constraint (str, optional): A string describing any constraint that the data should obey. Returns: Node: A Node object containing the data. @@ -24,11 +23,11 @@ def node(data, name=None, trainable=False, description=None, constraint=None): Notes: If trainable=True: - If data is already a Node, extracts underlying data and updates name - - Creates ParameterNode with extracted data, name, trainable=True and constraint + - Creates ParameterNode with extracted data, name, trainable=True If trainable=False: - If data is already a Node, returns it (with warning if name provided) - - Otherwise creates new Node with data, name and constraint + - Otherwise creates new Node with data, name """ assert type(description) is str or description is None @@ -42,7 +41,6 @@ def node(data, name=None, trainable=False, description=None, constraint=None): name=name, trainable=True, description=description, - constraint=constraint, ) else: if isinstance(data, Node): @@ -50,7 +48,7 @@ def node(data, name=None, trainable=False, description=None, constraint=None): warnings.warn(f"Name {name} is ignored because data is already a Node.") return data else: - return Node(data, name=name, description=description, constraint=constraint) + return Node(data, name=name, description=description) NAME_SCOPES = [] # A stack of name scopes @@ -763,21 +761,19 @@ class Node(AbstractNode[T]): name (str, optional): The name of the node. trainable (bool, optional): Whether the node is trainable or not. Defaults to False. description (str, optional): String describing the node. Defaults to "[Node] This is a node in a computational graph." - constraint (Union[None, str], optional): String describing constraints that the data should satisfy. Defaults to None. info (Union[None, Dict], optional): Dictionary containing additional information about the node. Defaults to None. Attributes: trainable (bool): Whether the node is trainable or not. _feedback (dict): Dictionary of feedback from children nodes. _description (str): String describing the node. - _constraint (str): String describing all constraints that the data should satisfy. _backwarded (bool): Whether the backward method has been called. _info (dict): Dictionary containing additional information about the node. _dependencies (dict): Dictionary of dependencies on parameters and expandable nodes. Notes: The Node class extends AbstractNode to represent a data node in a directed graph. - It includes attributes and methods to handle feedback, constraints, and dependencies. + It includes attributes and methods to handle feedback, description, and dependencies. The node can be marked as trainable and store feedback from children nodes. The feedback mechanism is analogous to gradients in machine learning and propagates information back through the graph. The feedback mechanism supports non-commutative @@ -792,8 +788,7 @@ def __init__( *, name: str = None, trainable: bool = False, - description: str = "[Node] This is a node in a computational graph.", - constraint: Union[None, str] = None, + description: str = None, info: Union[None, Dict] = None, ) -> None: """Initialize an instance of the Node class. @@ -803,12 +798,11 @@ def __init__( name: The name of the node (optional). trainable: A boolean indicating whether the node is trainable or not (optional). description: A string describing the node (optional). - constraint: A string describing constraints on the node (optional). info: A dictionary containing additional information about the node (optional). """ if description == "" or description is None: - description = "[Node] This is a node in a computational graph." + description = f"[Node] Data type: {type(value)}." matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: @@ -822,7 +816,6 @@ def __init__( # to support implementing aggregation that is not commutable. self._feedback = defaultdict(list) self._description = description - self._constraint = constraint self._backwarded = False self._info = info self._dependencies = {"parameter": set(), "expandable": set()} @@ -843,8 +836,10 @@ def feedback(self): @property def description(self): - """A textual description of the node.""" - return self._description + """A textual description of the node.""" + # return self._description + # remove the operator type from the description + return re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() @property def info(self): @@ -2006,7 +2001,6 @@ def __init__( name=None, trainable=True, description="[ParameterNode] This is a ParameterNode in a computational graph.", - constraint=None, projections=None, # a list of Projection info=None, ) -> None: @@ -2024,7 +2018,6 @@ def __init__( name=name, trainable=trainable, description=description, - constraint=constraint, info=info, ) self._dependencies["parameter"].add(self) @@ -2076,12 +2069,11 @@ def __init__( *, inputs: Union[List[Node], Dict[str, Node]], # extra description: str, - constraint=None, name=None, info=None, ) -> None: super().__init__( - value, name=name, description=description, constraint=constraint, info=info + value, name=name, description=description, info=info ) assert isinstance(inputs, list) or isinstance( @@ -2179,7 +2171,6 @@ def __init__( *, inputs: Union[List[Node], Dict[str, Node]], description: str = "[ExceptionNode] This is node containing the error of execution.", - constraint=None, name=None, info=None, ) -> None: @@ -2191,7 +2182,6 @@ def __init__( value, inputs=inputs, description=description, - constraint=constraint, name=name, info=info, ) From b41151f092ee962706499a2aecfb1daded1f2c4c Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 15:39:12 -0700 Subject: [PATCH 055/172] Update CONTRIBUTING.md --- CONTRIBUTING.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eff59005..7cd2df87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,19 +11,19 @@ Here is an outline: 1. `main` will be regularly updated by PRs based on the development of the `experimental` branch following the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). Each update will result in a version update. -2. Except for the planned roadmap, `main` will only be updated to fix bugs. Bug fix to what is in `main` should be submitted as PR to `main`. This will trigger a quicker review and result in a version update in the third digit, and the `experimental` branch will then rebase on the updated `main`. +2. Except for the planned roadmap, `main` will only be updated to fix bugs. Bug fix to what is in `main` should be submitted as PR to `main`. This will trigger a quicker review (< 3 days) and result in a version update in the third digit, and the `experimental` branch will then rebase on the updated `main`. -3. For feature development, PR should be submitted to the `experimental` branch without version update. Generally, the `experimental` branch aims to realize the milestones listed in the next version update in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). If applicable, new determinstic unit tests should be added under `tests/unit_tests`. Otherwise, an example run script should be added in `examples`. +3. For feature development, PR should be submitted to the `experimental` branch without version update. Generally, the `experimental` branch aims to realize the milestones listed in the next version update in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing). If applicable, new determinstic unit tests should be added under `tests/unit_tests`, or an example run script should be added in `examples`. -4. [**LIGHT**] Bugs fix to the new changes introduced in the `experimental` branch should be submitted as a PR to the `experimental` branch. This PR will be incoporated quickly with a light review. +4. [**LIGHT**] Bugs fix to the new changes introduced in the `experimental` branch should be submitted as a PR to the `experimental` branch. This PR will be incoporated quickly with a light review. 5. [**LIGHT**] For contributions under the directory `opto/features`, they should be submitted as PR to the `experimental` branch. These usually are not under roadmap and are content not made as dependable by codes in other directories. That is, contents under `opto/features/A` should not be imported by files other than those under `opto/features/A`. So long as this rule is met, the PR will be incorprated under a light review. -6. [Exception] Core contributors only: Updates to non-coding elements (like documents) do not necessarily require a PR +6. Updates to non-coding elements (like documents) do not require a PR. The above is applicable to all contributors, including the core contributors and maintainers. -In a regular development cycle, all features and bug fixes are merged into the experimental branch. After the items listed in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) are all added to the `experimental` branch, a version branch (e.g., `0.2.1`) will be created from `experimental`, and it will be staged for a release (to be merged into the `main` branch with a PR). At this point, the version number of the `experimental` branch will be updated to start the development of the next version. +In a regular development cycle, all features and bug fixes are merged into the experimental branch. After the items listed in the [roadmap doc](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) are all added to the `experimental` branch, a version branch (e.g., `v0.2.1`) will be created from `experimental`, and it will be staged for a release (to be merged into the `main` branch with a PR). At this point, the version number of the `experimental` branch will be updated to start the development of the next version. ![workflow](https://github.com/AgentOpt/Trace/blob/experimental/docs/images/contributing_workflow.png?raw=true) @@ -33,6 +33,10 @@ In a regular development cycle, all features and bug fixes are merged into the e 2. For bugs, feature requests, contributions, or questions that might be related to a broader audience, post them as issues on the github page. +### Other Branches + +In addition to `main` and `experimental`, other branches have a naming convention or `vx.x.x` for version branchs, or of `feature/xxx` or `fix/xxx`, which implements the items on the roadmap. They will be merged into the `main` or `experimental` accordingly following the rules above. Once merged, they will be removed. + # Steps for Contributions From 52558cf5c107b9e99622cf487551ae258897b019 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 03:35:09 +0000 Subject: [PATCH 056/172] Update default description. --- opto/trace/nodes.py | 15 +++++++++------ opto/trace/propagators/propagators.py | 2 +- tests/unit_tests/test_modules.py | 2 +- tests/unit_tests/test_nodes.py | 12 ++++++++---- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 8186175a..f10c3b16 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -802,7 +802,7 @@ def __init__( """ if description == "" or description is None: - description = f"[Node] Data type: {type(value)}." + description = f"[Node] type: {type(value)}" matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: @@ -840,6 +840,11 @@ def description(self): # return self._description # remove the operator type from the description return re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() + + @property + def op_name(self): + """The operator type of the node, extracted from the description.""" + return get_op_name(self._description) @property def info(self): @@ -1014,7 +1019,7 @@ def backward( # Plot the edge from parent to node # Bypass chain of identity operators (for better visualization) while ( - get_op_name(parent.description) in IDENTITY_OPERATORS + parent.op_name in IDENTITY_OPERATORS ) and simple_visualization: assert ( len(parent.parents) == 1 @@ -2000,14 +2005,12 @@ def __init__( *, name=None, trainable=True, - description="[ParameterNode] This is a ParameterNode in a computational graph.", + description=None, projections=None, # a list of Projection info=None, ) -> None: if description is None or description == "": - description = ( - "[ParameterNode] This is a ParameterNode in a computational graph." - ) + description = f"[ParameterNode] type: {type(value)}" matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: diff --git a/opto/trace/propagators/propagators.py b/opto/trace/propagators/propagators.py index 101f538f..9081f8d3 100644 --- a/opto/trace/propagators/propagators.py +++ b/opto/trace/propagators/propagators.py @@ -45,7 +45,7 @@ def register(self, operator_name, propagate_function): self.override[operator_name] = propagate_function def propagate(self, child: MessageNode) -> Dict[Node, Any]: - operator_name = get_op_name(child.description) + operator_name = child.op_name if operator_name in self.override: return self.override[operator_name](child) else: diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 1934ae5b..f08d26e5 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -239,7 +239,7 @@ def test_model_dump_with_projection(): try: # Test with BlackCodeFormatter from opto.trace.projections import BlackCodeFormatter - dummy.model_dump(temp_file, projection=BlackCodeFormatter()) + dummy.model_dump(temp_file, projections=[BlackCodeFormatter()]) with open(temp_file, "r") as f: content = f.read() # Check if content is properly formatted diff --git a/tests/unit_tests/test_nodes.py b/tests/unit_tests/test_nodes.py index b2b3a73f..129c26e8 100644 --- a/tests/unit_tests/test_nodes.py +++ b/tests/unit_tests/test_nodes.py @@ -145,16 +145,20 @@ def test_trainable_wrapping(): def test_node_description(): x = node(1, description="x") - assert x.description == "[Node] x" + assert x._description == "[Node] x" + assert x.description == "x" y = node(1) - assert y.description == '[Node] This is a node in a computational graph.' + assert y.description == "type: " + assert y._description == "[Node] type: " x = node(1, description="x", trainable=True) - assert x.description == "[ParameterNode] x" + assert x.description == "x" + assert x._description == "[ParameterNode] x" x = node(1, trainable=True) - assert x.description == "[ParameterNode] This is a ParameterNode in a computational graph." + assert x.description == "type: " + assert x._description == "[ParameterNode] type: " def test_iterating_numpy_array(): From 8f4327a7fc158ebcce952391da8647d37cdeb0fb Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 04:13:13 +0000 Subject: [PATCH 057/172] Fix some bugs due to description change. --- opto/optimizers/optoprime.py | 2 +- opto/trace/bundle.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 74e5d1f4..53513198 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -361,7 +361,7 @@ def problem_instance(self, summary, mask=None): else "" ), documentation=( - "\n".join([v for v in summary.documentation.values()]) + "\n".join([f"[{k}] {v}" for k, v in summary.documentation.items()]) if "#Documentation" not in mask else "" ), diff --git a/opto/trace/bundle.py b/opto/trace/bundle.py index 0e1c38ff..6d5fa5c8 100644 --- a/opto/trace/bundle.py +++ b/opto/trace/bundle.py @@ -164,7 +164,7 @@ def __init__( if description is None: # Generate the description from the function name and docstring. - description = f"[{self.info['fun_name']}] {self.info['doc']}." + description = f"[{self.info['fun_name']}] {self.info['doc']}" assert len(get_op_name(description)) > 0 self.traceable_code = traceable_code From 062137267db8f36db8363f7d37153cf6e72430fd Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 04:19:41 +0000 Subject: [PATCH 058/172] Make default description construction programmatic --- opto/trace/nodes.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index f10c3b16..6110974f 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -802,11 +802,11 @@ def __init__( """ if description == "" or description is None: - description = f"[Node] type: {type(value)}" + description = f"[{type(self).__name__}] type: {type(value)}" matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: - description = "[Node] " + description.strip() + description = f"[{type(self).__name__}] " + description.strip() super().__init__(value, name=name) self.trainable = trainable @@ -2009,12 +2009,6 @@ def __init__( projections=None, # a list of Projection info=None, ) -> None: - if description is None or description == "": - description = f"[ParameterNode] type: {type(value)}" - - matched = re.match(r"^\[([^\[\]]+)\]", description) - if not matched: - description = "[ParameterNode] " + description.strip() super().__init__( value, @@ -2173,7 +2167,7 @@ def __init__( value: Exception, *, inputs: Union[List[Node], Dict[str, Node]], - description: str = "[ExceptionNode] This is node containing the error of execution.", + description: str = None, name=None, info=None, ) -> None: From 3274fd4affe267b68ae71b58d3102bbbcbb90dfe Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 05:42:32 +0000 Subject: [PATCH 059/172] Set default description to be None. --- opto/trace/nodes.py | 11 +++++++---- tests/unit_tests/test_nodes.py | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 6110974f..26191b5e 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -587,8 +587,9 @@ def get_label(self, x): """ # using colon in the name causes problems in graphviz description = x.description - if len(x.description) > self.print_limit: - description = x.description[: self.print_limit] + "..." + description = '' if description is None else description + if len(description) > self.print_limit: + description = description[: self.print_limit] + "..." text = x.py_name + "\n" + description + "\n" content = str(x.data) @@ -802,7 +803,7 @@ def __init__( """ if description == "" or description is None: - description = f"[{type(self).__name__}] type: {type(value)}" + description = f"[{type(self).__name__}]" matched = re.match(r"^\[([^\[\]]+)\]", description) if not matched: @@ -839,7 +840,9 @@ def description(self): """A textual description of the node.""" # return self._description # remove the operator type from the description - return re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() + description = re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() + # return None if empty + return description if description else None @property def op_name(self): diff --git a/tests/unit_tests/test_nodes.py b/tests/unit_tests/test_nodes.py index 129c26e8..6d5d1e73 100644 --- a/tests/unit_tests/test_nodes.py +++ b/tests/unit_tests/test_nodes.py @@ -149,16 +149,16 @@ def test_node_description(): assert x.description == "x" y = node(1) - assert y.description == "type: " - assert y._description == "[Node] type: " + assert y.description == None + assert y._description == "[Node]" x = node(1, description="x", trainable=True) assert x.description == "x" assert x._description == "[ParameterNode] x" x = node(1, trainable=True) - assert x.description == "type: " - assert x._description == "[ParameterNode] type: " + assert x.description == None + assert x._description == "[ParameterNode]" def test_iterating_numpy_array(): From 3b872f18ab87b258c233cab81b4cb147da411aa5 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 05:44:52 +0000 Subject: [PATCH 060/172] Fix bug of textgrad seeing None description. --- opto/optimizers/textgrad.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index 722df049..b78353de 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -281,6 +281,7 @@ def rm_node_attrs(text: str) -> str: Returns: String with trace node attributes removed """ + text = "" if text is None else text return re.sub(r"\[.*?\]", "", text).strip() From f7d65afbe01e9d899ae9efcad554b5b58f370918 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 06:00:31 +0000 Subject: [PATCH 061/172] Fix a bug in textgrad due to earlier commit that removes constraint_text --- opto/optimizers/textgrad.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index b78353de..bdfdeab4 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -414,6 +414,7 @@ def _update_prompt(self, node: Node, gradients: List[GradientInfo]): "variable_value": node.data, "variable_grad": self._get_gradient_and_context_text(gradients), "variable_short": get_short_value(node.data), + "constraint_text": rm_node_attrs(node.description), "new_variable_start_tag": self.new_variable_tags[0], "new_variable_end_tag": self.new_variable_tags[1], # "in_context_examples": "\n".join(self.in_context_examples), From fac22ad262bf9cd8e449e75f185ce5d80f32a084 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 06:07:05 +0000 Subject: [PATCH 062/172] Update docstring of Node. --- opto/trace/nodes.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 26191b5e..756328e6 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -761,13 +761,13 @@ class Node(AbstractNode[T]): value (Any): The value to be assigned to the node. name (str, optional): The name of the node. trainable (bool, optional): Whether the node is trainable or not. Defaults to False. - description (str, optional): String describing the node. Defaults to "[Node] This is a node in a computational graph." + description (str, optional): String describing the node which acts as a soft constraint. Defaults to None. info (Union[None, Dict], optional): Dictionary containing additional information about the node. Defaults to None. Attributes: trainable (bool): Whether the node is trainable or not. _feedback (dict): Dictionary of feedback from children nodes. - _description (str): String describing the node. + _description (str): String describing the node. Defaults to "[Node]". _backwarded (bool): Whether the backward method has been called. _info (dict): Dictionary containing additional information about the node. _dependencies (dict): Dictionary of dependencies on parameters and expandable nodes. @@ -791,16 +791,7 @@ def __init__( trainable: bool = False, description: str = None, info: Union[None, Dict] = None, - ) -> None: - """Initialize an instance of the Node class. - - Args: - value: The value to be assigned to the node. - name: The name of the node (optional). - trainable: A boolean indicating whether the node is trainable or not (optional). - description: A string describing the node (optional). - info: A dictionary containing additional information about the node (optional). - """ + ) -> None: if description == "" or description is None: description = f"[{type(self).__name__}]" From e309dbe38ee1b1e0adf8d11adfc461301129111d Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Wed, 25 Jun 2025 23:09:00 -0700 Subject: [PATCH 063/172] Update ci.yml Apply CI to experimental branch --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f0d21b3..46e0b317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main, dev, ci-multi ] + branches: [ main, dev, experimental, ci-multi ] pull_request: - branches: [ main, dev, ci-multi ] + branches: [ main, dev, experimental, ci-multi ] jobs: test: From a21c91f7dee04ac7fcf14570dd780dcb312af236 Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Thu, 26 Jun 2025 10:17:24 -0700 Subject: [PATCH 064/172] Update CONTRIBUTING.md --- CONTRIBUTING.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7cd2df87..52182780 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,22 +51,20 @@ If there is a minor, isolated bug that can be directly fixed, please report it a We welcome new ideas. -### Step 1: Feature Spec Doc -A feature should first be written as a Google Doc (an example is [here](https://docs.google.com/document/d/1FX1ygc8lgFpFn3ni3E2A_DCGtn505PpAM8QaAjEovsA/edit?usp=sharing)). +### Step 1: Create an Issue -### Step 2: Create an Issue -An issue should be created, and under the issue, the doc is linked. People should be allowed to comment on the doc. +If your changes are expected to involve in less 50 lines of codes, create an issue titled "[LIGHT] XXX". You should describe the motivation, give an overview of the change (e.g., by a pseudo-code) and its desired effects. Otherwise, create an issue titled "[MAJOR] XXX". You should write a more detailed description of your motivation, design, and demo. If more space is needed, you can attach a link to a Google doc. People should be allowed to comment on the doc. -### Step 3: Implement Feature -Create a separate branch, extending from the `experimental` branch. This branch contains all the new features that have not been merged into the `main` branch yet. +### Step 2: Implement Feature + +Create a separate branch, extending from the `experimental` branch, which contains all the new features that have not been merged into the `main` branch yet. Make sure your features are implemented, along with `unit tests` or `examples` to show how it's used. -### Step 4: Create a Pull Request -Create a PR formally to merge into the experiment branch and request a review. For standalone features, put the changes under `opto/features/`. This will trigger the lightest review that only checks for malicious code, or if the feature does not pass its own unit tests. -For changes to the rest, expect a slightly longer review process as we work out how the changes should be integrated with the core library. +### Step 3: Create a Pull Request +Create a PR formally to merge into the experiment branch and request a review. For standalone features, put the changes under `opto/features/`. This will trigger the lightest review that only checks for malicious code, or if the feature does not pass its own unit tests. For changes to the rest, expect a slightly longer review process as we work out how the changes should be integrated with the core library. Also, [LIGHT] issues can expect faster review than [MAJOR]. -### Step 5: Merge into Experimental +### Step 4: Merge into Experimental Once the request is approved, it will be merged into the `experimental` branch. From 3360598b43ef88a88c9daa00295c2dbb5a93a6ed Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 21:09:29 +0000 Subject: [PATCH 065/172] Fix the bug in test_modules.py --- tests/unit_tests/test_modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 1934ae5b..f08d26e5 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -239,7 +239,7 @@ def test_model_dump_with_projection(): try: # Test with BlackCodeFormatter from opto.trace.projections import BlackCodeFormatter - dummy.model_dump(temp_file, projection=BlackCodeFormatter()) + dummy.model_dump(temp_file, projections=[BlackCodeFormatter()]) with open(temp_file, "r") as f: content = f.read() # Check if content is properly formatted From a7c44b97593467de340604288a1bcc8fbe570744 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 26 Jun 2025 22:09:35 +0000 Subject: [PATCH 066/172] Move and uipdate evaluate to incorporate deepcopy and num_samples --- opto/trainer/algorithms/basic_algorithms.py | 40 ++------------------ opto/trainer/evaluators.py | 42 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 opto/trainer/evaluators.py diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 4bc8247d..281bcf02 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -6,43 +6,9 @@ from opto.trainer.loader import DataLoader from opto.trainer.utils import async_run from opto.optimizers.utils import print_color +from opto.trainer.evaluators import evaluate -def evaluate(agent, guide, inputs, infos, min_score=None, num_threads=None, description=None): - """ Evaluate the agent on the inputs and return the scores - - Args: - agent: The agent to evaluate - guide: The guide to use for evaluation - inputs: List of inputs to evaluate on - infos: List of additional information for each input - min_score: Minimum score to return when an exception occurs - num_threads: Maximum number of threads to use for parallel evaluation - description: Description to display in the progress bar - """ - - def evaluate_single(i): - try: - output = agent(inputs[i]).data - score = guide.metric(inputs[i], output, infos[i]) - except: - score = min_score - return score - - N = len(inputs) - assert len(inputs) == len(infos), "Inputs and infos must have the same length" - # Use asyncio if num_threads is not None and > 1 - use_asyncio = num_threads is not None and num_threads > 1 - if use_asyncio: - # Use provided description or generate a default one - eval_description = description or f"Evaluating {N} examples" - scores = async_run([evaluate_single] * N, [(i,) for i in range(N)], - max_workers=num_threads, - description=eval_description) # list of tuples - else: - scores = [evaluate_single(i) for i in range(N)] - return scores - def standard_optimization_step(agent, x, guide, info, min_score=0): """ Forward and compute feedback. @@ -93,6 +59,7 @@ def train(self, batch_size: int = 1, # batch size for updating the agent test_dataset = None, # dataset of (x, info) pairs to evaluate the agent eval_frequency: int = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input log_frequency: Union[int, None] = None, # frequency of logging save_frequency: Union[int, None] = None, # frequency of saving the agent save_path: str = "checkpoints/agent.pkl", # path to save the agent @@ -112,6 +79,7 @@ def train(self, num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided use_asyncio = self._use_asyncio(num_threads) + self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input # Evaluate the agent before learning if eval_frequency > 0: @@ -184,7 +152,7 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_threads=None, de """ Evaluate the agent on the given dataset. """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, - description=description) + description=description, num_samples=self.num_eval_samples) if all([s is not None for s in test_scores]): return np.mean(test_scores) diff --git a/opto/trainer/evaluators.py b/opto/trainer/evaluators.py new file mode 100644 index 00000000..db9e35ec --- /dev/null +++ b/opto/trainer/evaluators.py @@ -0,0 +1,42 @@ +from opto.trainer.utils import async_run +import copy + + +def evaluate(agent, guide, inputs, infos, min_score=None, num_samples=1, num_threads=None, description=None): + """ Evaluate the agent on the inputs and return the scores + + Args: + agent: The agent to evaluate + guide: The guide to use for evaluation + inputs: List of inputs to evaluate on + infos: List of additional information for each input + min_score: Minimum score to return when an exception occurs + num_samples: Number of samples to use to evaluate each input + num_threads: Maximum number of threads to use for parallel evaluation + description: Description to display in the progress bar + """ + + def evaluate_single(agent, guide, i): + try: + output = agent(inputs[i]).data + score = guide.metric(inputs[i], output, infos[i]) + except: + score = min_score + return score + + N = len(inputs) + assert len(inputs) == len(infos), "Inputs and infos must have the same length" + # Use asyncio if num_threads is not None and > 1 + use_asyncio = num_threads is not None and num_threads > 1 + + # repeat each index num_samples times + indices = [i for i in range(N) for _ in range(num_samples)] + if use_asyncio: + # Use provided description or generate a default one + eval_description = description or f"Evaluating {N} examples" + scores = async_run([evaluate_single] * N, [(copy.deepcopy(agent), copy.deepcopy(guide), i) for i in indices], + max_workers=num_threads, + description=eval_description) # list of tuples + else: + scores = [evaluate_single(agent, guide, i) for i in indices] + return scores \ No newline at end of file From 7b63ba79ba232460fdc8098a01034de8a2d3d0ce Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 27 Jun 2025 00:47:57 -0700 Subject: [PATCH 067/172] fix typing to be `Optional[int]` instead of `int` --- opto/trainer/algorithms/algorithm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 9ec35fcc..e08eec44 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -1,4 +1,6 @@ import warnings +from typing import Optional + from opto import trace from opto.trace.modules import Module from opto.trainer.utils import async_run @@ -28,7 +30,7 @@ class AlgorithmBase(AbstractAlgorithm): def __init__(self, agent, # trace.model - num_threads: int = None, # maximum number of threads to use for parallel execution + num_threads: Optional[int] = None, # maximum number of threads to use for parallel execution logger=None, # logger for tracking metrics *args, **kwargs): From 38779614f5c24458ea89700c6d5249dcb7135e56 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 27 Jun 2025 21:16:14 +0000 Subject: [PATCH 068/172] Fix a bug that BasicSearchAlgorithm always prints to stdout. --- opto/trainer/algorithms/basic_algorithms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 4bc8247d..84d78a31 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -341,7 +341,7 @@ def validate(): # TODO perhaps we can ask for multiple updates in one query or use different temperatures in different queries # Generate different proposals - step_kwargs = dict(bypassing=True, verbose='output') # we don't print the inner full message + step_kwargs = dict(bypassing=True, verbose='output' if verbose else False) # we don't print the inner full message step_kwargs.update(kwargs) # update with additional kwargs if provided use_asyncio = self._use_asyncio() if use_asyncio: From cfd3e558b32629d11cf8fe492d11330a644fe8db Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 30 Jun 2025 23:03:50 +0000 Subject: [PATCH 069/172] Add batch_run --- opto/trace/modules.py | 9 ++++ opto/trainer/utils.py | 69 ++++++++++++++++++++++++++ tests/unit_tests/test_batch_run.py | 78 ++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 tests/unit_tests/test_batch_run.py diff --git a/opto/trace/modules.py b/opto/trace/modules.py index bf33d6a3..6b7f0114 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -107,6 +107,15 @@ def forward(self, *args, **kwargs): def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) + + def copy(self): + """Return a deep copy of the module except for the parameters + are set to the originals.""" + new_module = copy.deepcopy(self) + for k, v in self.parameters_dict().items(): + if hasattr(new_module, k): + setattr(new_module, k, v) + return new_module def save(self, file_name: str): """Save the parameters of the model to a pickle file.""" diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index 717ff23b..ea838954 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -4,6 +4,7 @@ from concurrent.futures import ThreadPoolExecutor from tqdm.asyncio import tqdm_asyncio from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES +from opto.trace.modules import Module def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None): """Run multiple functions in asynchronously. @@ -47,6 +48,74 @@ async def _run(): return asyncio.run(_run()) +def batch_run(fun, max_workers=None, description=None): + """ + Create a function that runs in parallel using asyncio, with support for batching. + The batch size is inferred as the length of the longest argument or keyword argument. + + Args: + fun (callable): The function to run. + + max_workers (int, optional): Maximum number of worker threads to use. + If None, the default ThreadPoolExecutor behavior is used. + description (str, optional): Description to display in the progress bar. + + Returns: + callable: A new function that processes batches of inputs. + + NOTE: + If fun takes input that has __len__ (like lists or arrays), they won't be broadcasted. + When using batch_run, be sure to pass list of such arguments of the same length. + + Example: + >>> @batch_run(max_workers=4, description="Processing batch") + >>> def my_function(x, y): + >>> return x + y + >>> x = [1, 2, 3, 4, 5] + >>> y = 10 + >>> outputs = my_function(x, y) + >>> # outputs will be [11, 12, 13, 14, 15] + >>> # This will run the function in asynchronously with 4 threads + """ + + + def _fun(*args, **kwargs): + + # We try to infer the batch size from the args + all_args = args + tuple(kwargs.values()) + # find all list or array-like arguments and use their length as batch size + batch_size = max(len(arg) for arg in all_args if hasattr(arg, '__len__')) + + # broadcast the batch size to all args and record the indices that are broadcasted + args = [arg if hasattr(arg, '__len__') else [arg] * batch_size for arg in args] + kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} + + # assert that all args and kwargs have the same length + lengths = [len(arg) for arg in args] + [len(v) for v in kwargs.values()] + if len(set(lengths)) != 1: + raise ValueError("All arguments and keyword arguments must have the same length.") + + # deepcopy if it is a trace.Module (as they may have mutable state) + # Module.copy() is used to create a new instance with the same parameters + _args = [arg.copy() if isinstance(arg, Module) else arg for arg in args] + _kwargs = {k: v.copy() if isinstance(v, Module) else v for k, v in kwargs.items()} + + # Run the forward function in parallel using asyncio with the same parameters. + # Since trace.Node is treated as immutable, we can safely use the same instance. + # The resultant graph will be the same as if we had called the function with the original arguments. + + # convert _args and _kwargs (args, kwargs of list) to lists of args and kwargs + + args_list = [tuple(aa[i] for aa in _args) for i in range(batch_size)] + kwargs_list = [{k: _kwargs[k][i] for k in _kwargs} for i in range(batch_size)] + + outputs = async_run([fun] * batch_size, args_list=args_list, kwargs_list=kwargs_list, + max_workers=max_workers, description=description) + return outputs + + return _fun + + if __name__ == "__main__": def tester(t): # regular time-consuming function diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py new file mode 100644 index 00000000..ffee737b --- /dev/null +++ b/tests/unit_tests/test_batch_run.py @@ -0,0 +1,78 @@ +from typing import List +from opto import trace +from opto.trainer.utils import batch_run + +def test_batch_run_fun(): + + def fun(x, y): + return x + y + + # Create a batch of inputs + x = [1, 2, 3, 4, 5] + y = 10 # this will be broadcasted to each element in x + + # Run the function in batch mode + outputs = batch_run(fun, max_workers=3)(x,y) + assert outputs == [11, 12, 13, 14, 15], f"Expected [11, 12, 13, 14, 15], got {outputs}" + + # Handling a function taking a list as inputs + def fun(x: List[int], y: List[int]) -> List[int]: + return [a + b for a, b in zip(x, y)] + + x = [[1, 2, 3], [4, 5, 6]] + y = [10, 20, 30] # list won't be braodcasted correctly + + raise_error = False + try: + outputs = batch_run(fun, max_workers=3)(x, y) + except ValueError as e: + assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" + raise_error = True + assert raise_error, "Expected a ValueError but did not get one." + + # Now we can broadcast y to match the length of x + y = [[10, 20, 30]] * len(x) # Broadcast + outputs = batch_run(fun, max_workers=3)(x, y) + assert outputs == [[11, 22, 33], [14, 25, 36]], f"Expected [[11, 22, 33], [14, 25, 36]], got {outputs}" + + + y = [10, 20] # This will raise an error because x and y have different lengths + raise_error = False + try: + outputs = batch_run(fun, max_workers=3)(x, y) + except TypeError as e: + raise_error = True + assert raise_error, "Expected a TypeError but did not get one." + +def test_batch_run_module(): + + + @trace.model + class MyModule: + def __init__(self, param): + self.param = trace.node(param, trainable=True) + self._state = 0 + + def forward(self, x): + y = x + self.param + self._state += 1 # This should not affect the batch run + return y + + module = MyModule(10) + x = [1, 2, 3, 4, 5] + outputs = batch_run(module.forward, max_workers=3)(x) + assert outputs == [11, 12, 13, 14, 15], f"Expected [11, 12, 13, 14, 15], got {outputs}" + param = module.parameters()[0] + assert len(param.children) == 5 + + + x = [1, 2, 3, 4, 5] + y = [10, 20, 30, 40, 50, 60] + # This should raise an error because x and y have different lengths + raise_error = False + try: + outputs = batch_run(module.forward, max_workers=3)(x, y) + except ValueError as e: + assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" + raise_error = True + assert raise_error, "Expected a ValueError but did not get one." \ No newline at end of file From b18ad2a2f40b3cd7dd58879d1c13a1ec92e5b053 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 30 Jun 2025 23:09:21 +0000 Subject: [PATCH 070/172] add allow_sequential_run to async_run. --- opto/trainer/utils.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index ea838954..bebed5b3 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -6,7 +6,7 @@ from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES from opto.trace.modules import Module -def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None): +def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None, allow_sequential_run=True): """Run multiple functions in asynchronously. Args: @@ -17,7 +17,7 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de If None, the default ThreadPoolExecutor behavior is used. description (str, optional): description to display in the progress bar. This can indicate the current stage (e.g., "Evaluating", "Training", "Optimizing"). - + allow_sequential_run (bool, optional): if True, runs the functions sequentially if max_workers is 1. """ # if ALLOW_EXTERNAL_DEPENDENCIES is not False: # warnings.warn( @@ -27,25 +27,26 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de # UserWarning, # ) - if args_list is None: args_list = [[]] * len(runs) if kwargs_list is None: kwargs_list = [{}] * len(runs) - async def _run(): - loop = asyncio.get_event_loop() - with ThreadPoolExecutor(max_workers=max_workers) as executor: - tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) - for run, args, kwargs, in zip(runs, args_list, kwargs_list)] - - # Use the description in the tqdm progress bar if provided - if description: - return await tqdm_asyncio.gather(*tasks, desc=description) - else: - return await tqdm_asyncio.gather(*tasks) - - return asyncio.run(_run()) + if (max_workers == 1) and allow_sequential_run: # run without asyncio + return [run(*args, **kwargs) for run, args, kwargs in zip(runs, args_list, kwargs_list)] + else: + async def _run(): + loop = asyncio.get_event_loop() + with ThreadPoolExecutor(max_workers=max_workers) as executor: + tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) + for run, args, kwargs, in zip(runs, args_list, kwargs_list)] + + # Use the description in the tqdm progress bar if provided + if description: + return await tqdm_asyncio.gather(*tasks, desc=description) + else: + return await tqdm_asyncio.gather(*tasks) + return asyncio.run(_run()) def batch_run(fun, max_workers=None, description=None): From a9f26d491b5ef56b7dadb3fd4d2cc914c377f97a Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 30 Jun 2025 23:28:37 +0000 Subject: [PATCH 071/172] Update batch_run. --- opto/trainer/guide.py | 11 ++++ opto/trainer/utils.py | 99 +++++++++++++++++------------- tests/unit_tests/test_batch_run.py | 14 +++-- 3 files changed, 76 insertions(+), 48 deletions(-) diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 30c428a6..cad39f37 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -1,6 +1,7 @@ from typing import List, Dict, Any, Union, Tuple, Optional, Callable import json import re +import copy from opto.utils.llm import LLM, AbstractModel from opto.trainer.suggest import Suggest @@ -43,6 +44,16 @@ def get_feedback(self, query: str, response: str, reference: Optional[str] = Non def metric(self, query: str, response: str, reference: Optional[str] = None, **kwargs) -> float: """ Exact match metric """ return self.get_feedback(query, response, reference)[0] + + def copy(): + """ Create a copy of the guide instance. + + Returns: + A new instance of the same guide class with the same parameters. + """ + # This is used in batch_run to create a new instance of the guide. + # This can be overridden by subclasses to provide a more specific copy behavior. + return copy.deepcopy(self) class VerbalJudgeGuide(AutoGuide): diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index bebed5b3..93436505 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -5,6 +5,7 @@ from tqdm.asyncio import tqdm_asyncio from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES from opto.trace.modules import Module +from opto.trainer.guide import AutoGuide def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None, allow_sequential_run=True): """Run multiple functions in asynchronously. @@ -49,7 +50,7 @@ async def _run(): return asyncio.run(_run()) -def batch_run(fun, max_workers=None, description=None): +def batch_run(max_workers=None, description=None): """ Create a function that runs in parallel using asyncio, with support for batching. The batch size is inferred as the length of the longest argument or keyword argument. @@ -72,50 +73,64 @@ def batch_run(fun, max_workers=None, description=None): >>> @batch_run(max_workers=4, description="Processing batch") >>> def my_function(x, y): >>> return x + y - >>> x = [1, 2, 3, 4, 5] - >>> y = 10 - >>> outputs = my_function(x, y) - >>> # outputs will be [11, 12, 13, 14, 15] - >>> # This will run the function in asynchronously with 4 threads + >>> x = [1, 2, 3, 4, 5] + >>> y = 10 + >>> outputs = my_function(x, y) + >>> # outputs will be [11, 12, 13, 14, 15] + >>> # This will run the function in asynchronously with 4 threads """ - - def _fun(*args, **kwargs): - - # We try to infer the batch size from the args - all_args = args + tuple(kwargs.values()) - # find all list or array-like arguments and use their length as batch size - batch_size = max(len(arg) for arg in all_args if hasattr(arg, '__len__')) + def decorator(fun): + """ + Decorator to create a function that runs in parallel using asyncio, with support for batching. - # broadcast the batch size to all args and record the indices that are broadcasted - args = [arg if hasattr(arg, '__len__') else [arg] * batch_size for arg in args] - kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} - - # assert that all args and kwargs have the same length - lengths = [len(arg) for arg in args] + [len(v) for v in kwargs.values()] - if len(set(lengths)) != 1: - raise ValueError("All arguments and keyword arguments must have the same length.") - - # deepcopy if it is a trace.Module (as they may have mutable state) - # Module.copy() is used to create a new instance with the same parameters - _args = [arg.copy() if isinstance(arg, Module) else arg for arg in args] - _kwargs = {k: v.copy() if isinstance(v, Module) else v for k, v in kwargs.items()} - - # Run the forward function in parallel using asyncio with the same parameters. - # Since trace.Node is treated as immutable, we can safely use the same instance. - # The resultant graph will be the same as if we had called the function with the original arguments. - - # convert _args and _kwargs (args, kwargs of list) to lists of args and kwargs - - args_list = [tuple(aa[i] for aa in _args) for i in range(batch_size)] - kwargs_list = [{k: _kwargs[k][i] for k in _kwargs} for i in range(batch_size)] - - outputs = async_run([fun] * batch_size, args_list=args_list, kwargs_list=kwargs_list, - max_workers=max_workers, description=description) - return outputs - - return _fun - + Args: + fun (callable): The function to run. + + max_workers (int, optional): Maximum number of worker threads to use. + If None, the default ThreadPoolExecutor behavior is used. + description (str, optional): Description to display in the progress bar. + + Returns: + callable: A new function that processes batches of inputs. + """ + def _fun(*args, **kwargs): + + # We try to infer the batch size from the args + all_args = args + tuple(kwargs.values()) + # find all list or array-like arguments and use their length as batch size + batch_size = max(len(arg) for arg in all_args if hasattr(arg, '__len__')) + + # broadcast the batch size to all args and record the indices that are broadcasted + args = [arg if hasattr(arg, '__len__') else [arg] * batch_size for arg in args] + kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} + + # assert that all args and kwargs have the same length + lengths = [len(arg) for arg in args] + [len(v) for v in kwargs.values()] + if len(set(lengths)) != 1: + raise ValueError("All arguments and keyword arguments must have the same length.") + + # deepcopy if it is a trace.Module (as they may have mutable state) + # Module.copy() is used to create a new instance with the same parameters + _args = [arg.copy() if isinstance(arg, (Module, AutoGuide)) else arg for arg in args] + _kwargs = {k: v.copy() if isinstance(v, (Module, AutoGuide)) else v for k, v in kwargs.items()} + + # Run the forward function in parallel using asyncio with the same parameters. + # Since trace.Node is treated as immutable, we can safely use the same instance. + # The resultant graph will be the same as if we had called the function with the original arguments. + + # convert _args and _kwargs (args, kwargs of list) to lists of args and kwargs + + args_list = [tuple(aa[i] for aa in _args) for i in range(batch_size)] + kwargs_list = [{k: _kwargs[k][i] for k in _kwargs} for i in range(batch_size)] + + outputs = async_run([fun] * batch_size, args_list=args_list, kwargs_list=kwargs_list, + max_workers=max_workers, description=description) + return outputs + + return _fun + + return decorator if __name__ == "__main__": diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py index ffee737b..714caac3 100644 --- a/tests/unit_tests/test_batch_run.py +++ b/tests/unit_tests/test_batch_run.py @@ -4,6 +4,7 @@ def test_batch_run_fun(): + @batch_run(max_workers=3) def fun(x, y): return x + y @@ -12,10 +13,11 @@ def fun(x, y): y = 10 # this will be broadcasted to each element in x # Run the function in batch mode - outputs = batch_run(fun, max_workers=3)(x,y) + outputs = fun(x,y) assert outputs == [11, 12, 13, 14, 15], f"Expected [11, 12, 13, 14, 15], got {outputs}" # Handling a function taking a list as inputs + @batch_run(max_workers=3) def fun(x: List[int], y: List[int]) -> List[int]: return [a + b for a, b in zip(x, y)] @@ -24,7 +26,7 @@ def fun(x: List[int], y: List[int]) -> List[int]: raise_error = False try: - outputs = batch_run(fun, max_workers=3)(x, y) + outputs = fun(x, y) except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" raise_error = True @@ -32,14 +34,14 @@ def fun(x: List[int], y: List[int]) -> List[int]: # Now we can broadcast y to match the length of x y = [[10, 20, 30]] * len(x) # Broadcast - outputs = batch_run(fun, max_workers=3)(x, y) + outputs = fun(x, y) assert outputs == [[11, 22, 33], [14, 25, 36]], f"Expected [[11, 22, 33], [14, 25, 36]], got {outputs}" y = [10, 20] # This will raise an error because x and y have different lengths raise_error = False try: - outputs = batch_run(fun, max_workers=3)(x, y) + outputs = fun(x, y) except TypeError as e: raise_error = True assert raise_error, "Expected a TypeError but did not get one." @@ -60,7 +62,7 @@ def forward(self, x): module = MyModule(10) x = [1, 2, 3, 4, 5] - outputs = batch_run(module.forward, max_workers=3)(x) + outputs = batch_run(max_workers=3)(module.forward)(x) assert outputs == [11, 12, 13, 14, 15], f"Expected [11, 12, 13, 14, 15], got {outputs}" param = module.parameters()[0] assert len(param.children) == 5 @@ -71,7 +73,7 @@ def forward(self, x): # This should raise an error because x and y have different lengths raise_error = False try: - outputs = batch_run(module.forward, max_workers=3)(x, y) + outputs = batch_run(max_workers=3)(module.forward)(x, y) except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" raise_error = True From 2deb8c2021ba3c1fbba179ed04fba63058f45d5b Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 30 Jun 2025 23:29:41 +0000 Subject: [PATCH 072/172] Fix the typo bug in gsm8k example --- examples/gsm8k_trainer_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index f9524dc0..7b627674 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -71,7 +71,7 @@ def main(): test_dataset = train_dataset agent = Learner(llm=LLM(student_model)) - guide = Guide(model=LLM(teacher_model)) + guide = Guide(llm=LLM(teacher_model)) optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) logger = Logger(verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format From 02924d2434b48ec18a8fbee60ab08be74f4585bf Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 1 Jul 2025 00:03:13 +0000 Subject: [PATCH 073/172] Update evaluate to use batch_run. --- opto/trainer/evaluators.py | 29 ++++++++--------- tests/unit_tests/test_batch_run.py | 52 ++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/opto/trainer/evaluators.py b/opto/trainer/evaluators.py index db9e35ec..309189b5 100644 --- a/opto/trainer/evaluators.py +++ b/opto/trainer/evaluators.py @@ -1,4 +1,5 @@ -from opto.trainer.utils import async_run +from opto.trainer.utils import async_run, batch_run +from opto.trace import ExecutionError import copy @@ -15,28 +16,24 @@ def evaluate(agent, guide, inputs, infos, min_score=None, num_samples=1, num_thr num_threads: Maximum number of threads to use for parallel evaluation description: Description to display in the progress bar """ + assert len(inputs) == len(infos), "Inputs and infos must have the same length" + N = len(inputs) + # Use provided description or generate a default one + eval_description = description or f"Evaluating {N} examples" - def evaluate_single(agent, guide, i): + @batch_run(max_workers=num_threads, description=eval_description) + def _evaluate(agent, guide, i): try: output = agent(inputs[i]).data score = guide.metric(inputs[i], output, infos[i]) - except: + except ExecutionError as e: score = min_score return score - N = len(inputs) - assert len(inputs) == len(infos), "Inputs and infos must have the same length" - # Use asyncio if num_threads is not None and > 1 - use_asyncio = num_threads is not None and num_threads > 1 - # repeat each index num_samples times indices = [i for i in range(N) for _ in range(num_samples)] - if use_asyncio: - # Use provided description or generate a default one - eval_description = description or f"Evaluating {N} examples" - scores = async_run([evaluate_single] * N, [(copy.deepcopy(agent), copy.deepcopy(guide), i) for i in indices], - max_workers=num_threads, - description=eval_description) # list of tuples - else: - scores = [evaluate_single(agent, guide, i) for i in indices] + + # Run the evaluation in parallel + scores = _evaluate(agent, guide, indices) + return scores \ No newline at end of file diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py index 714caac3..74e3c149 100644 --- a/tests/unit_tests/test_batch_run.py +++ b/tests/unit_tests/test_batch_run.py @@ -37,15 +37,10 @@ def fun(x: List[int], y: List[int]) -> List[int]: outputs = fun(x, y) assert outputs == [[11, 22, 33], [14, 25, 36]], f"Expected [[11, 22, 33], [14, 25, 36]], got {outputs}" - - y = [10, 20] # This will raise an error because x and y have different lengths - raise_error = False - try: - outputs = fun(x, y) - except TypeError as e: - raise_error = True - assert raise_error, "Expected a TypeError but did not get one." - + # This will raise an error because x and y have different lengths + # y = [10, 20] + # outputs = fun(x, y) + def test_batch_run_module(): @@ -77,4 +72,41 @@ def forward(self, x): except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" raise_error = True - assert raise_error, "Expected a ValueError but did not get one." \ No newline at end of file + assert raise_error, "Expected a ValueError but did not get one." + + +def test_evaluate(): + # This test the evaluate function in opto.trainer.evaluators built on top of batch_run + from opto.trainer.evaluators import evaluate + from opto.trainer.guide import AutoGuide + from opto import trace + + @trace.model + class MyAgent: + def __init__(self, param): + self.param = trace.node(param, trainable=True) + + def forward(self, x): + y = x + self.param + self.param += 1 # This should not affect the batch run + return y + + class MyGuide(AutoGuide): + def __init__(self, param): + super().__init__() + self.param = param + + def get_feedback(self, query, response, reference=None): + score = float(response == query + self.param + reference) + feedback = f"Score: {score}, Response: {response}, Query: {query}" + print(score, feedback) + self.param += 1 # This should not affect the batch run + return score, feedback + + agent = MyAgent(10) + guide = MyGuide(10) + inputs = [1, 2, 3, 4, 5] + infos = [0, 1, 2, 3, 4] # These are the expected outputs (query + param + info) + evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=1, num_threads=1) + expected_scores = [1, 0, 0, 0, 0] # All inputs should match the expected outputs + assert evaluated_scores == expected_scores, f"Expected {expected_scores}, got {evaluated_scores}" \ No newline at end of file From fa1044daa31a66f8a1cadb88f18d3e078560421b Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 1 Jul 2025 18:26:02 +0000 Subject: [PATCH 074/172] Update algo to use batch_run. Update evaluate to return 2d array when num_samples >1. --- opto/trainer/algorithms/__init__.py | 2 ++ opto/trainer/algorithms/algorithm.py | 4 --- opto/trainer/algorithms/basic_algorithms.py | 34 +++++++------------ .../algorithms/beamsearch_algorithm.py | 34 ++++++------------- opto/trainer/evaluators.py | 9 +++-- opto/trainer/utils.py | 1 + tests/unit_tests/test_batch_run.py | 8 +++-- 7 files changed, 40 insertions(+), 52 deletions(-) diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index ea5dde63..2586fd31 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1 +1,3 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm +from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm +from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index e08eec44..7995fc0b 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -1,9 +1,5 @@ -import warnings from typing import Optional - -from opto import trace from opto.trace.modules import Module -from opto.trainer.utils import async_run from opto.trainer.loggers import DefaultLogger import os diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index ae27d955..8ec0eb4f 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -4,7 +4,7 @@ from opto import trace from opto.trainer.algorithms.algorithm import AlgorithmBase from opto.trainer.loader import DataLoader -from opto.trainer.utils import async_run +from opto.trainer.utils import batch_run, async_run from opto.optimizers.utils import print_color from opto.trainer.evaluators import evaluate @@ -78,7 +78,6 @@ def train(self, log_frequency = log_frequency or eval_frequency # frequency of logging (default to eval_frequency) num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided - use_asyncio = self._use_asyncio(num_threads) self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input # Evaluate the agent before learning @@ -104,13 +103,8 @@ def train(self, backup_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} # Forward the agent on the inputs and compute the feedback using the guide - if use_asyncio: # Run forward asynchronously - outputs = async_run([self.forward]*len(xs), - [(self.agent, x, guide, info) for x, info in zip(xs, infos)], - max_workers=num_threads, - description=f"Forward pass (batch size: {len(xs)})") # async forward - else: # Run forward sequentially - outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs, infos) ] + forward = batch_run(max_workers=num_threads, description=f"Forward pass (batch size: {len(xs)})")(self.forward) + outputs = forward(self.agent, xs, guide, infos) # Update the agent score = self.update(outputs, verbose=verbose, num_threads=num_threads, **kwargs) @@ -148,14 +142,14 @@ def train(self, return train_scores, test_score - def evaluate(self, agent, guide, xs, infos, min_score=None, num_threads=None, description=None): + def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_threads=None, description=None): """ Evaluate the agent on the given dataset. """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, - description=description, num_samples=self.num_eval_samples) + num_samples=num_samples, description=description, num_samples=self.num_eval_samples) if all([s is not None for s in test_scores]): return np.mean(test_scores) - + def has_improvement(self, xs, guide, infos, current_score, current_outputs, backup_dict, threshold=0, num_threads=None, *args, **kwargs): # This function can be overridden by subclasses to implement their own improvement check. """ Check if the updated agent is improved compared to the current one. @@ -311,15 +305,13 @@ def validate(): # Generate different proposals step_kwargs = dict(bypassing=True, verbose='output' if verbose else False) # we don't print the inner full message step_kwargs.update(kwargs) # update with additional kwargs if provided - use_asyncio = self._use_asyncio() - if use_asyncio: - update_dicts = async_run([super().optimizer_step]*self.num_proposals, - kwargs_list=[step_kwargs] * self.num_proposals, - max_workers=num_threads, - description=f"Generating {self.num_proposals} proposals") # async step - else: - update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(self.num_proposals)] - + + # Use aysnc_run to run the optimizer_step in parallel + # NOTE optimizer_step is coupled via async_run + update_dicts = async_run([super().optimizer_step]*self.num_proposals, + kwargs_list=[step_kwargs] * self.num_proposals, + max_workers=num_threads, + description=f"Generating {self.num_proposals} proposals") # async step # Validate the proposals candidates = [] backup_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} # backup the current value diff --git a/opto/trainer/algorithms/beamsearch_algorithm.py b/opto/trainer/algorithms/beamsearch_algorithm.py index 0451455e..0beac524 100644 --- a/opto/trainer/algorithms/beamsearch_algorithm.py +++ b/opto/trainer/algorithms/beamsearch_algorithm.py @@ -1,7 +1,7 @@ import numpy as np import copy from typing import Union, List, Tuple, Dict, Any, Optional -from opto.trainer.utils import async_run +from opto.trainer.utils import async_run, batch_run from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify @@ -329,15 +329,9 @@ def expand(self, xs_batch, infos_batch = self._sample_minibatch(train_dataset, batch_size) # Forward the agent on the minibatch - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - outputs = async_run([self.forward]*len(xs_batch), - [(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)], - max_workers=num_threads, - description=f"Forward pass (beam {beam_idx+1}, batch size: {len(xs_batch)})") - else: - outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)] - + forward = batch_run(max_workers=num_threads, description=f"Forward pass (batch size: {len(xs_batch)})")(self.forward) + outputs = forward(self.agent, xs_batch, guide, infos_batch) + # Prepare for optimizer backward and step scores, targets, feedbacks = [], [], [] for target, score, feedback in outputs: @@ -356,13 +350,10 @@ def expand(self, candidates = [] # Generate num_proposals candidates - if use_asyncio: - update_dicts = async_run([self.optimizer.step]*num_proposals, - kwargs_list=[step_kwargs] * num_proposals, - max_workers=num_threads, - description=f"Generating {num_proposals} proposals for beam {beam_idx+1}") - else: - update_dicts = [self.optimizer.step(**step_kwargs) for _ in range(num_proposals)] + update_dicts = async_run([self.optimizer.step]*num_proposals, + kwargs_list=[step_kwargs] * num_proposals, + max_workers=num_threads, + description=f"Generating {num_proposals} proposals for beam {beam_idx+1}") # Collect all valid proposals for update_dict in update_dicts: @@ -721,12 +712,9 @@ def expand(self, use_asyncio = self._use_asyncio(num_threads) description=f"Forward pass (beam {beam_idx+1}, batch size: {len(xs_batch)})" - if use_asyncio: - outputs = async_run([self.forward]*len(xs_batch), - [(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)], - max_workers=num_threads, description=description) - else: - outputs = [self.forward(self.agent, x, guide, info) for x, info in zip(xs_batch, infos_batch)] + + forward = batch_run(max_workers=num_threads, description=description)(self.forward) + outputs = forward(self.agent, xs_batch, guide, infos_batch) # Prepare original feedback scores, targets, feedbacks = [], [], [] diff --git a/opto/trainer/evaluators.py b/opto/trainer/evaluators.py index 309189b5..d1e99c8e 100644 --- a/opto/trainer/evaluators.py +++ b/opto/trainer/evaluators.py @@ -1,6 +1,7 @@ -from opto.trainer.utils import async_run, batch_run +from opto.trainer.utils import batch_run from opto.trace import ExecutionError import copy +import numpy as np def evaluate(agent, guide, inputs, infos, min_score=None, num_samples=1, num_threads=None, description=None): @@ -35,5 +36,9 @@ def _evaluate(agent, guide, i): # Run the evaluation in parallel scores = _evaluate(agent, guide, indices) - + scores = np.array(scores) + if num_samples > 1: + # scores will be of length N * num_samples + # Reshape scores into an array of shape (N, num_samples) + scores = scores.reshape(N, num_samples) return scores \ No newline at end of file diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index 93436505..16067b78 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -34,6 +34,7 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de kwargs_list = [{}] * len(runs) if (max_workers == 1) and allow_sequential_run: # run without asyncio + print(f"{description} (Running sequentially).") return [run(*args, **kwargs) for run, args, kwargs in zip(runs, args_list, kwargs_list)] else: async def _run(): diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py index 74e3c149..5da10ddb 100644 --- a/tests/unit_tests/test_batch_run.py +++ b/tests/unit_tests/test_batch_run.py @@ -99,7 +99,6 @@ def __init__(self, param): def get_feedback(self, query, response, reference=None): score = float(response == query + self.param + reference) feedback = f"Score: {score}, Response: {response}, Query: {query}" - print(score, feedback) self.param += 1 # This should not affect the batch run return score, feedback @@ -109,4 +108,9 @@ def get_feedback(self, query, response, reference=None): infos = [0, 1, 2, 3, 4] # These are the expected outputs (query + param + info) evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=1, num_threads=1) expected_scores = [1, 0, 0, 0, 0] # All inputs should match the expected outputs - assert evaluated_scores == expected_scores, f"Expected {expected_scores}, got {evaluated_scores}" \ No newline at end of file + assert (evaluated_scores == expected_scores).all(), f"Expected {expected_scores}, got {evaluated_scores}" + + + evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=2, num_threads=1) + expected_scores = [[1, 1], [0, 0], [0, 0], [0, 0], [0, 0]] # Each input should match the expected outputs twice + assert (evaluated_scores == expected_scores).all(), f"Expected {expected_scores}, got {evaluated_scores.tolist()}" \ No newline at end of file From 03c7a9e94492e27299594d9cfb905034a595826a Mon Sep 17 00:00:00 2001 From: Ching-An Cheng Date: Tue, 1 Jul 2025 14:27:22 -0700 Subject: [PATCH 075/172] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e5e94e9a..724d6014 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ git is unable to clone the repository. ## Updates -- **2025.5.9** Adith Swaminathan gave a talk at Netflix Workshop on Personalization, Recommendation and Search (PRS)[https://prs2025.splashthat.com/] -- **2025.5.1** Ching-An Cheng gave a talk at 2nd Texas Colloquium on Distributed Learning (TL;DR)[https://sites.google.com/view/tldr-2025] +- **2025.5.28** Datarobot released Efficient Search for Pareto-optimal Flows [syftr](https://github.com/datarobot/syftr) powered by Trace. +- **2025.5.9** Adith Swaminathan gave a talk at [Netflix Workshop on Personalization, Recommendation and Search (PRS)](https://prs2025.splashthat.com/) +- **2025.5.1** Ching-An Cheng gave a talk at [2nd Texas Colloquium on Distributed Learning (TL;DR)](https://sites.google.com/view/tldr-2025) - **2025.2.7** Trace was featured in the [G-Research NeurIPS highlight](https://www.gresearch.com/news/neurips-paper-reviews-2024-8/) by the Science Director Hugh Salimbeni. - **2024.12.10** Trace was demoed in person at NeurIPS 2024 Expo. - **2024.11.05** Ching-An Cheng gave a talk at UW Robotics Colloquium on Trace: [video](https://www.youtube.com/watch?v=T2g1Vo3u_9g). From 66be4a4cf9cc560fc1559ef6c034959792661fc1 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 1 Jul 2025 22:18:16 +0000 Subject: [PATCH 076/172] Add flags to set json keys in OptoPrime. Add DummyLLM for testing. Merge changes in OptoPrimeV2 as flag in OptoPrime. --- opto/optimizers/optoprime.py | 81 ++++++++++++++++++++--- opto/utils/llm.py | 30 +++++++++ tests/unit_tests/test_optoprime_update.py | 54 +++++++++++++++ 3 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 tests/unit_tests/test_optoprime_update.py diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 5a5c5c36..730892bf 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -168,29 +168,50 @@ class OptoPrime(Optimizer): # Optimization default_objective = "You need to change the of the variables in #Variables to improve the output in accordance to #Feedback." - output_format_prompt = dedent( + output_format_prompt_original = dedent( """ Output_format: Your output should be in the following json format, satisfying the json syntax: {{ - "reasoning": , - "answer": , - "suggestion": {{ + "{reasoning}": , + "{answer}": , + "{suggestion}": {{ : , : , }} }} - In "reasoning", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + In "{reasoning}", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. - If #Instruction asks for an answer, write it down in "answer". + If #Instruction asks for an answer, write it down in "{answer}". - If you need to suggest a change in the values of #Variables, write down the suggested values in "suggestion". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + If you need to suggest a change in the values of #Variables, write down the suggested values in "{suggestion}". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. If no changes or answer are needed, just output TERMINATE. """ ) + output_format_prompt_no_answer = dedent( + """ + Output_format: Your output should be in the following json format, satisfying the json syntax: + + {{ + "{reasoning}": , + "{suggestion}": {{ + : , + : , + }} + }} + + In "{reasoning}", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + + If you need to suggest a change in the values of #Variables, write down the suggested values in "{suggestion}". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + + If no changes are needed, just output TERMINATE. + """ + ) + + example_problem_template = dedent( """ Here is an example of problem instance and response: @@ -234,6 +255,14 @@ class OptoPrime(Optimizer): """ ) + final_prompt_with_variables = dedent( + """ + What are your suggestions on variables {names}? + + Your response: + """ + ) + default_prompt_symbols = { "variables": "#Variables", "constraints": "#Constraints", @@ -246,6 +275,12 @@ class OptoPrime(Optimizer): "documentation": "#Documentation", } + default_json_keys = { + "reasoning": "reasoning", + "answer": "answer", + "suggestion": "suggestion", + } + def __init__( self, parameters: List[ParameterNode], @@ -259,7 +294,9 @@ def __init__( max_tokens=4096, log=True, prompt_symbols=None, + json_keys=None, # keys to use in the json object format (can remove "answer" if not needed) use_json_object_format=True, # whether to use json object format for the response when calling LLM + highlight_variables=False, # whether to highlight the variables at the end in the prompt **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) @@ -295,7 +332,17 @@ def __init__( self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) if prompt_symbols is not None: self.prompt_symbols.update(prompt_symbols) + if json_keys is not None: + self.default_json_keys.update(json_keys) + if self.default_json_keys['answer'] is None: # answer field is not needed + del self.default_json_keys['answer'] + if 'answer' not in self.default_json_keys: + # If 'answer' is not in the json keys, we use the no-answer format + self.output_format_prompt = self.output_format_prompt_no_answer.format(**self.default_json_keys) + else: # If 'answer' is in the json keys, we use the original format of OptoPrime + self.output_format_prompt = self.output_format_prompt_original.format(**self.default_json_keys) self.use_json_object_format = use_json_object_format + self.highlight_variables = highlight_variables def default_propagator(self): """Return the default Propagator object of the optimizer.""" @@ -403,7 +450,17 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): ) + user_prompt ) - user_prompt += self.final_prompt + + + if self.highlight_variables: + var_names = [] + for k, v in summary.variables.items(): + var_names.append(f"{k}") # ({type(v[0]).__name__}) + var_names = ", ".join(var_names) + + user_prompt += self.final_prompt_with_variables.format(names=var_names) + else: # This is the original OptoPrime prompt + user_prompt += self.final_prompt # Add examples if len(self.memory) > 0: @@ -494,11 +551,13 @@ def construct_update_dict( def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" + suggestion_tag = self.default_json_keys["suggestion"] + suggestion = {} attempt_n = 0 while attempt_n < 2: try: - suggestion = json.loads(response)["suggestion"] + suggestion = json.loads(response)[suggestion_tag] break except json.JSONDecodeError: # Remove things outside the brackets @@ -514,7 +573,7 @@ def extract_llm_suggestion(self, response: str): if len(suggestion) == 0: # we try to extract key/value separately and return it as a dictionary - pattern = r'"suggestion"\s*:\s*\{(.*?)\}' + pattern = rf'"{suggestion_tag}"\s*:\s*\{{(.*?)\}}' suggestion_match = re.search(pattern, str(response), re.DOTALL) if suggestion_match: suggestion = {} @@ -530,7 +589,7 @@ def extract_llm_suggestion(self, response: str): if len(suggestion) == 0: if not self.ignore_extraction_error: - print("Cannot extract suggestion from LLM's response:") + print(f"Cannot extract {self.default_json_keys['suggestion']} from LLM's response:") print(response) # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index 3ae2f2c5..320ba2b2 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -313,6 +313,36 @@ def get_profile_info(cls, profile: str = None): return cls._profiles.get(profile) return cls._profiles + +class DummyLLM(AbstractModel): + """A dummy LLM that does nothing. Used for testing purposes.""" + + def __init__(self, + callable, + reset_freq: Union[int, None] = None) -> None: + # self.message = message + self.callable = callable + factory = lambda: self._factory() + super().__init__(factory, reset_freq) + + def _factory(self): + + # set response.choices[0].message.content + # create a fake container with above format + + class Message: + def __init__(self, content): + self.content = content + class Choice: + def __init__(self, content): + self.message = Message(content) + class Response: + def __init__(self, content): + self.choices = [Choice(content)] + + return lambda *args, **kwargs: Response(self.callable(*args, **kwargs)) + + class LLM: """ A unified entry point for all supported LLM backends. diff --git a/tests/unit_tests/test_optoprime_update.py b/tests/unit_tests/test_optoprime_update.py new file mode 100644 index 00000000..0b273d2f --- /dev/null +++ b/tests/unit_tests/test_optoprime_update.py @@ -0,0 +1,54 @@ +from opto import trace +from opto.optimizers import OptoPrime +from opto.utils.llm import DummyLLM + + + +def test_json_keys(): + """ + Test that the OptoPrimeV2 class correctly initializes with json_keys. + """ + param = trace.node(1, trainable=True) + + def callable(messages, **kwargs): + format_prompt = """Output_format: Your output should be in the following json format, satisfying the json syntax: + +{ +"reasoning_mod": , +"suggestion_mod": { + : , + : , +} +} + +In "reasoning_mod", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + +If you need to suggest a change in the values of #Variables, write down the suggested values in "suggestion_mod". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature.""" + assert format_prompt in messages[0]['content'] # system + assert '"answer":' not in messages[0]['content'] + highlight_prompt = "What are your suggestions on variables int0?" + assert highlight_prompt in messages[1]['content'] # user + return "Dummy response" #messages + + llm = DummyLLM(callable) + + optimizer = OptoPrime( + parameters=[param], + llm = llm, + json_keys=dict( + reasoning="reasoning_mod", + answer=None, + suggestion="suggestion_mod"), + highlight_variables=True, + ) + + + y = param + 10 + optimizer.zero_feedback() + optimizer.backward(y, 'dummy feedback') + optimizer.step(verbose=True) + + + + + From 6fcb72b4b49a0349e97ecd3a2cc17e73e5165267 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 1 Jul 2025 18:23:41 -0700 Subject: [PATCH 077/172] contribute an interesting test case for the module copy behavior --- tests/unit_tests/test_modules.py | 123 +++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index f08d26e5..ae1e9267 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -400,3 +400,126 @@ def multiply(self, x, y): finally: if os.path.exists(temp_file): os.remove(temp_file) + +def test_copy_function(): + """Test the copy function of Module class.""" + + @model + class TestCopyClass: + def __init__(self): + super().__init__() + self._param = node(10, trainable=True) + self.regular_attr = "original_value" + self.list_attr = [1, 2, 3] + self.dict_attr = {"key": "value"} + + @bundle(trainable=True) + def test_method(self, x): + return x + self._param + + def forward(self, x): + return self.test_method(x) + + # Create original instance + original = TestCopyClass() + original.regular_attr = "modified_value" + original.list_attr.append(4) + original.dict_attr["new_key"] = "new_value" + + # Create a copy + copied = original.copy() + + # Test that it's a different object + assert copied is not original + + # Test that regular attributes are copied (deep copy) + assert copied.regular_attr == "modified_value" + assert copied.list_attr == [1, 2, 3, 4] + assert copied.dict_attr == {"key": "value", "new_key": "new_value"} + + # Test that parameters are references to the original parameters + assert copied._param is original._param + assert copied.test_method.parameter is original.test_method.parameter + + # Test that modifying the original parameter affects the copy + original._param._data = 20 + assert copied._param._data == 20 + + # Test that modifying the copy's parameter affects the original + copied._param._data = 30 + assert original._param._data == 30 + + # Test that the copy can still function + result = copied.forward(5) + assert result._data == 35 # 5 + 30 + + # Test that modifying regular attributes doesn't affect the original + copied.regular_attr = "copy_only_value" + assert original.regular_attr == "modified_value" + + # Test that modifying list/dict attributes doesn't affect the original (deep copy) + copied.list_attr.append(5) + assert len(original.list_attr) == 4 + assert len(copied.list_attr) == 5 + + copied.dict_attr["copy_only"] = "copy_value" + assert "copy_only" not in original.dict_attr + assert "copy_only" in copied.dict_attr + +def test_copy_function_with_nested_modules(): + """Test the copy function with nested modules.""" + + @model + class NestedModule: + def __init__(self): + super().__init__() + self._nested_param = node(5, trainable=True) + + @bundle(trainable=True) + def nested_method(self, x): + return x * self._nested_param + + def forward(self, x): + return self.nested_method(x) + + @model + class ParentModule: + def __init__(self): + super().__init__() + self._param = node(10, trainable=True) + self._nested = NestedModule() + self.regular_attr = "parent_value" + + @bundle(trainable=True) + def parent_method(self, x): + return self._nested.forward(x) + self._param + + def forward(self, x): + return self.parent_method(x) + + # Create original instance + original = ParentModule() + original.regular_attr = "modified_parent" + original._nested._nested_param._data = 7 + + # Create a copy + copied = ParentModule() + copied = original.copy() + + # Test that it's a different object + assert copied is not original + + # Test that nested module is copied but parameters are references + assert copied._nested is not original._nested # Different object + assert copied._nested._nested_param is original._nested._nested_param # Same parameter reference + + # Test that regular attributes are copied + assert copied.regular_attr == "modified_parent" + + # Test that modifying nested parameter affects both + original._nested._nested_param._data = 8 + assert copied._nested._nested_param._data == 8 + + # Test that the copy can still function + result = copied.forward(3) + assert result._data == 34 # (3 * 8) + 10 From c287836dc5930dd162f39de10d88f8848dca0ada Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Wed, 2 Jul 2025 00:41:17 -0500 Subject: [PATCH 078/172] add the latest version of UCB search algorithms --- opto/trainer/algorithms/UCBsearch.py | 1513 ++++++++++++++++++++++++++ 1 file changed, 1513 insertions(+) create mode 100644 opto/trainer/algorithms/UCBsearch.py diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py new file mode 100644 index 00000000..0f3f9bc3 --- /dev/null +++ b/opto/trainer/algorithms/UCBsearch.py @@ -0,0 +1,1513 @@ +import numpy as np +import copy +import math +from collections import deque +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trainer.utils import async_run # Assuming print_color is in utils +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful +import json # For LLM output parsing +import random # Added for alpha probability +from opto.utils.llm import LLM # For the selector LLM +from opto.trace.nodes import ParameterNode +import warnings +from black import format_str, FileMode + +class UCBSearchAlgorithm(MinibatchAlgorithm): + """ + UCB Search Algorithm. + + Keeps a buffer of candidates with their statistics (score sum, evaluation count). + In each iteration: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a''. + 4. Evaluates 'a'' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a'' (with its validation stats) to the buffer. + 7. If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, # Controls exploration vs exploitation tradeoff in UCB selection + # UCB formula: μ(a) + c * sqrt(ln(t) / n(a)), c is the exploration factor + logger=None, + num_threads: int = None, + use_validation: bool = False, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, + # lower values favor exploitation of well-performing candidates. + self.ucb_exploration_factor = ucb_exploration_factor + self.use_validation = use_validation # Whether to use validation set for evaluation + # To ensure optimizer_step can be called with bypassing=True if needed. + # This depends on the specific optimizer's implementation. + # For now, we assume the optimizer has a step method that can return parameters. + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 # Tracks total number of individual candidate evaluations used in UCB calculation for log(T) + self._candidate_id_counter = 0 + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], # Changed from validate_dataset + guide, # Changed from validate_guide + evaluation_batch_size: int, # New parameter name + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset (now typically train_dataset).""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + self.optimizer.update(params_to_eval_dict) + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, # Use main guide + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else 0 + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') # Explore unvisited states first + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + # UCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _calculate_lcb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates Lower Confidence Bound for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('-inf') # Unvisited states get lowest bound + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + # LCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score - exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def _get_best_candidate_from_buffer(self, buffer): + """Get the best candidate from buffer, excluding those with eval_count = 0 when not using validation.""" + if not buffer: + return None + + # Filter out candidates with eval_count = 0 if not using validation + if not self.use_validation: + valid_candidates = [c for c in buffer if c['eval_count'] > 0] + if not valid_candidates: + # If no candidates have been evaluated, return the one with highest UCB score + return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) + return max(valid_candidates, key=lambda c: c['score_sum'] / c['eval_count']) + else: + # When using validation, all candidates should have eval_count > 0 + return max(buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) + + def print_intervals(self, buffer): + """Print confidence intervals for debugging in the form of open intervals (LCB, UCB)""" + print_color("Confidence intervals for all candidates:", 'cyan') + for i, candidate_entry in enumerate(buffer): + lcb = self._calculate_lcb(candidate_entry, self._total_evaluations_tracker) + ucb = candidate_entry['ucb_score'] + mean_score = candidate_entry['score_sum'] / (candidate_entry['eval_count'] or 1) + eval_count = candidate_entry['eval_count'] + + # Format as open interval (LCB, UCB) with mean score and evaluation count + interval_str = f"Action {i+1}: ({lcb:.4f}, {ucb:.4f}) [mean: {mean_score:.4f}, n: {eval_count}]" + print_color(interval_str, 'cyan') + + def _process_single_candidate(self, + action_candidate_a: Dict, + guide, + train_dataset: Dict[str, List[Any]], + validation_dataset: Dict[str, List[Any]], + train_batch_size: int, + evaluation_batch_size: int, + num_threads: Optional[int], + iteration: int) -> Tuple[bool, float, float, int]: + """ + Process a single candidate: generate a_prime, evaluate both a and a_prime, + update stats for 'a', and add 'a_prime' to buffer. + + Returns: + Tuple of (success, a_prime_score, score_for_a_on_train_batch, samples_used) + """ + # 2. Load parameters of 'a' into the agent for the optimizer update step + self.optimizer.update(action_candidate_a['params']) + + # 3. Draw minibatch from the training set, do update from 'a' to get 'a_prime' + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration}: Training minibatch empty for candidate, skipping.", 'yellow') + return False, -np.inf, -np.inf, 0 + + # Perform forward pass and get feedback for agent parameters 'a' + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration}: Forward pass for action 'a'") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: + print_color(f"Iter {iteration}: No outputs from forward pass for candidate. Skipping.", 'yellow') + return False, -np.inf, -np.inf, 0 + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) + + try: + a_prime_params_dict = self.optimizer.step(bypassing=True, verbose=False) + if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: + print_color(f"Iter {iteration}: Optimizer.step did not return valid params. Using current agent params.", 'yellow') + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + self.total_proposals += 1 + except Exception as e: + print_color(f"Iter {iteration}: Error during optimizer.step: {e}. Skipping.", 'red') + return False, -np.inf, -np.inf, 0 + + # 4. Evaluate 'a' and 'a_prime' on samples of validation set in parallel + if self.use_validation: + if use_asyncio: + evaluation_results = async_run( + [self._evaluate_candidate, self._evaluate_candidate], + [ + (action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads), + (a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads) + ], + max_workers=2, + description=f"Iter {iteration}: Parallel evaluation of 'a' and 'a_prime'" + ) + (a_score, a_evals), (a_prime_score, a_prime_evals) = evaluation_results + else: + a_score, a_evals = self._evaluate_candidate( + action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads + ) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + + # 5. Update statistics for the original candidate 'a' + # Always update statistics for the original candidate 'a' on the training set + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) + action_candidate_a['eval_count'] += len(train_xs) + self._total_evaluations_tracker += len(train_xs) + + # If we use validation set for evaluation + if self.use_validation: # If we use validation set for evaluation + action_candidate_a['score_sum'] += a_score * a_evals + action_candidate_a['eval_count'] += a_evals + + # 6. Add 'a_prime' to the buffer (with eviction logic if needed) + if a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, + 'eval_count': a_prime_evals, + 'ucb_score': None, # Will be updated later + 'iteration_created': iteration + } + + # Eviction logic before adding if buffer is at max capacity + if len(self.buffer) >= self.max_buffer_size: + self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate to buffer (score: {a_prime_score:.4f})", 'magenta') + else: + print_color(f"Iter {iteration}: New candidate a_prime had invalid score/evals, not added to buffer.", 'yellow') + + # Update tracking + self._total_evaluations_tracker += a_evals + a_prime_evals + samples_used = 2 * evaluation_batch_size + train_batch_size + else: # If we don't use validation set for evaluation, please evaluate a_prime on the training set + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, {'inputs': train_xs, 'infos': train_infos}, + guide, len(train_xs), num_threads + ) + self._total_evaluations_tracker += a_prime_evals + + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals if a_prime_score > -np.inf else 0, + 'eval_count': a_prime_evals, + 'ucb_score': None, # Will be updated later + 'iteration_created': iteration + } + self.buffer.append(new_candidate_entry) + samples_used = 2*train_batch_size # One batch for training update, one for evaluation + return True, a_prime_score, score_for_a_on_train_batch, samples_used + + def train(self, + guide, # Guide for train_dataset (feedback generation AND evaluation) + train_dataset: Dict[str, List[Any]], + *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset + test_dataset: Optional[Dict[str, List[Any]]] = None, + num_search_iterations: int = 100, + train_batch_size: int = 2, + evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_agent.pkl", + min_score_for_agent_update: Optional[float] = None, # Renamed from min_score to avoid conflict with evaluate's min_score + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + print_confidence_interval: bool = True, + **kwargs + ) -> Tuple[Dict[str, Any], float]: # Returns metrics and best score + """ + Main training loop for UCB Search Algorithm. + """ + # Default validation_dataset to train_dataset if not provided + if validation_dataset is None: + validation_dataset = train_dataset + if test_dataset is None: + test_dataset = train_dataset + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate + total_samples = 0 + self.total_proposals = 0 + # Metrics tracking + metrics = { + 'best_candidate_scores': [], # Score of the best candidate (e.g., highest mean) found so far at each iteration + 'selected_action_ucb': [], # UCB score of the selected action 'a' + 'new_candidate_scores': [], # Score of the new candidate 'a_prime' + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + } + +# 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide + ) + self.logger.log('Test score', initial_score, 0, color='blue') + self.logger.log('Total samples', total_samples, 0, color='cyan') + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + if self.use_validation: + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + # Log initial evaluation + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later + 'eval_count': initial_evals, + 'ucb_score': None, # avoid accidental reads before it's initialized + 'iteration_created': 0 + } + self._update_buffer_ucb_scores() # Update UCB for the initial candidate + else: + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': 0, + 'eval_count': 0, + 'ucb_score': None, # avoid accidental reads before it's initialized + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + try: + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + # 1. Pick the candidate 'a' with the highest UCB from the buffer + self._update_buffer_ucb_scores() # Ensure UCB scores are fresh + + action_candidate_a = self.select(self.buffer) + if print_confidence_interval: + self.print_intervals(self.buffer) + # Log selected action UCB score + self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') + self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') + + print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') + + # Process the selected candidate + success, a_prime_score, score_for_a_on_train_batch, samples_used = self._process_single_candidate( + action_candidate_a, guide, train_dataset, validation_dataset, + train_batch_size, evaluation_batch_size, num_threads, iteration + ) + + if not success: # Error occurred in processing + continue + + total_samples += samples_used + if self.use_validation: + metrics['new_candidate_scores'].append(a_prime_score) + self.logger.log('New candidate score', a_prime_score, iteration, color='green') + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}", 'cyan') + self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') + + + + # Update all UCB scores in the buffer after potential additions/removals/stat updates + self._update_buffer_ucb_scores() + + # Logging + best_in_buffer = self._get_best_candidate_from_buffer(self.buffer) + if best_in_buffer: + metrics['best_candidate_scores'].append(best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1)) + else: + metrics['best_candidate_scores'].append(-np.inf) + metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], #best_candidate_score_in_buffer + "selected_action_ucb": action_candidate_a['ucb_score'], + "new_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_tracker": self._total_evaluations_tracker, # used in calculating ucb scores + "total_samples": total_samples # Add new metric + } + + # Log all important metrics + self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') + self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') + self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') + self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') + # self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') + self.logger.log('Total samples', log_data['total_samples'], iteration, color='yellow') + self.logger.log('Total proposals', self.total_proposals, iteration, color='red') + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') + + if test_dataset is not None and iteration % eval_frequency == 0: + try: + # Save current agent parameters + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + + # Find the best candidate in the buffer (highest mean score) + best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_candidate: + print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') + continue + + # Load best candidate's parameters into the agent for evaluation + self.optimizer.update(best_candidate['params']) + + # Evaluate the best candidate on test set + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description=f"Evaluating best candidate (iteration {iteration})") + + # Restore original agent parameters + self.optimizer.update(current_params) + + self.logger.log('Test score', test_score, iteration, color='green') + except Exception as e: + print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') + + # Save agent (e.g., the one with highest mean score in buffer) + if save_frequency is not None and iteration % save_frequency == 0: + try: + best_overall_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_overall_candidate: + print_color(f"Iter {iteration}: No valid candidate for agent save.", 'yellow') + continue + self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer + self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') + except Exception as e: + print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') + + except Exception as e: + print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') + self.logger.log('Iteration error', str(e), iteration, color='red') + continue + + # End of search loop + print_color("UCB search finished.", 'blue') + + # Log final training summary + final_iteration = num_search_iterations + self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') + self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') + + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') + return metrics, -np.inf + + # Select the best candidate based on highest mean score (exploitation) + final_best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not final_best_candidate: + print_color("No valid candidate found at the end of search.", 'red') + return metrics, -np.inf + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + + # Log final results + self.logger.log('Final best score', final_best_score, final_iteration, color='green') + self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') + self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') + + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') + + # Load best parameters into the agent + self.optimizer.update(final_best_candidate['params']) # Load params using optimizer + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Could be subclassed to implement different selection strategies''' + return max(buffer, key=lambda c: c['ucb_score']) + + +class UCBSearchParallelAlgorithm(UCBSearchAlgorithm): + """ + Parallel UCB Search Algorithm. + + Instead of selecting one candidate with highest UCB score, selects top-k candidates + and processes them in parallel to generate k new candidates per iteration. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, + parallel_k: int = 2, # Number of top candidates to process in parallel + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, max_buffer_size, ucb_exploration_factor, + logger, num_threads, *args, **kwargs) + self.parallel_k = parallel_k + + def select_top_k(self, buffer, k): + """Select top k candidates with highest UCB scores""" + if len(buffer) <= k: + return buffer.copy() + + # Sort by UCB score and return top k + sorted_candidates = sorted(buffer, key=lambda c: c['ucb_score'], reverse=True) + return sorted_candidates[:k] + + def train(self, + guide, + train_dataset: Dict[str, List[Any]], + *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, + test_dataset: Optional[Dict[str, List[Any]]] = None, + num_search_iterations: int = 100, + train_batch_size: int = 2, + evaluation_batch_size: int = 20, + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_parallel_agent.pkl", + min_score_for_agent_update: Optional[float] = None, + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + print_confidence_interval: bool = True, + **kwargs + ) -> Tuple[Dict[str, Any], float]: + """ + Main training loop for Parallel UCB Search Algorithm. + """ + # Default validation_dataset to train_dataset if not provided + if validation_dataset is None: + validation_dataset = train_dataset + if test_dataset is None: + test_dataset = train_dataset + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update + total_samples = 0 + self.total_proposals = 0 + + # Metrics tracking + metrics = { + 'best_candidate_scores': [], + 'selected_actions_ucb': [], # UCB scores of selected top-k actions + 'new_candidate_scores': [], # Scores of all new candidates + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + 'parallel_k_used': [], # Track how many candidates were actually processed + } + + # Initialize with first candidate (same as parent) + print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + # Log initial evaluation + self.logger.log('Initial UCB score', initial_score, 0, color='blue') + self.logger.log('Total samples', total_samples, 0, color='cyan') + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, + 'eval_count': initial_evals, + 'ucb_score': None, + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + try: + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + # 1. Select top-k candidates with highest UCB scores + self._update_buffer_ucb_scores() + top_k_candidates = self.select_top_k(self.buffer, self.parallel_k) + + if print_confidence_interval: + self.print_intervals(self.buffer) + + print_color(f"Iter {iteration}/{num_search_iterations}: Processing {len(top_k_candidates)} candidates in parallel", 'blue') + + # Log selected actions UCB scores + selected_ucb_scores = [c['ucb_score'] for c in top_k_candidates] + metrics['selected_actions_ucb'].append(selected_ucb_scores) + avg_selected_ucb = np.mean(selected_ucb_scores) + self.logger.log('Average selected UCB', avg_selected_ucb, iteration, color='magenta') + + # 2. Process all top-k candidates sequentially + candidate_results = [] + for candidate in top_k_candidates: + result = self._process_single_candidate( + candidate, guide, train_dataset, validation_dataset, + train_batch_size, evaluation_batch_size, num_threads, iteration + ) + candidate_results.append(result) + + # 3. Process results and update statistics + iteration_new_scores = [] + + for i, (candidate, result) in enumerate(zip(top_k_candidates, candidate_results)): + success, a_prime_score, score_for_a_on_train_batch, samples_used = result + + if not success: # Error occurred + print_color(f"Iter {iteration}: Candidate {i+1} processing failed, skipping.", 'yellow') + continue + # Track new candidate score + iteration_new_scores.append(a_prime_score) + + # Update tracking + total_samples += samples_used + + metrics['new_candidate_scores'].extend(iteration_new_scores) + + # Log iteration performance + if iteration_new_scores: + avg_new_score = np.mean(iteration_new_scores) + max_new_score = max(iteration_new_scores) + self.logger.log('New candidate score', avg_new_score, iteration, color='green') #average new candidate score + self.logger.log('Max new candidate score', max_new_score, iteration, color='green') + print_color(f"Iter {iteration}: Generated {len(iteration_new_scores)} new candidates. Avg score: {avg_new_score:.4f}, Max: {max_new_score:.4f}", 'cyan') + + # Update UCB scores and track metrics + self._update_buffer_ucb_scores() + + if self.buffer: + best_in_buffer = self._get_best_candidate_from_buffer(self.buffer) + if best_in_buffer: + best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1) + metrics['best_candidate_scores'].append(best_score) + else: + metrics['best_candidate_scores'].append(-np.inf) + metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + + # Logging + if iteration % log_frequency == 0: + self.logger.log('Best candidate score', best_score, iteration, color='green') + self.logger.log('Buffer size', len(self.buffer), iteration, color='blue') + self.logger.log('Buffer average score', metrics['buffer_avg_score'][-1], iteration, color='cyan') + self.logger.log('Total samples', total_samples, iteration, color='yellow') + self.logger.log('Total proposals', self.total_proposals, iteration, color='red') + print_color(f"Log @ Iter {iteration}: Best score: {best_score:.4f}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') + + # Test evaluation (same as parent) + if test_dataset is not None and iteration % eval_frequency == 0: + try: + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_candidate: + print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') + continue + self.optimizer.update(best_candidate['params']) + + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description=f"Evaluating best candidate (iteration {iteration})") + + self.optimizer.update(current_params) + self.logger.log('Test score', test_score, iteration, color='green') + except Exception as e: + print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') + + # Save agent (same as parent) + if save_frequency is not None and iteration % save_frequency == 0: + try: + best_overall_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_overall_candidate: + print_color(f"Iter {iteration}: No valid candidate for agent save.", 'yellow') + continue + self.optimizer.update(best_overall_candidate['params']) + self.save_agent(save_path, iteration) + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') + except Exception as e: + print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') + + except Exception as e: + print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') + self.logger.log('Iteration error', str(e), iteration, color='red') + continue + + # End of search (same as parent) + print_color("Parallel UCB search finished.", 'blue') + + final_iteration = num_search_iterations + self.logger.log('Parallel UCB search completed', final_iteration, final_iteration, color='blue') + self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') + + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + return metrics, -np.inf + + final_best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not final_best_candidate: + print_color("No valid candidate found at the end of search.", 'red') + return metrics, -np.inf + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + + self.logger.log('Final best score', final_best_score, final_iteration, color='green') + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') + + # Load best parameters into the agent + self.optimizer.update(final_best_candidate['params']) + + return metrics, float(final_best_score) + + +class HybridUCB_LLM(MinibatchAlgorithm): + """ + UCB Search Algorithm with Function Approximation (LLM). + + Keeps a buffer of candidates. + In each iteration: + - With probability alpha: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a_prime'. + 4. Evaluates 'a_prime' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a_prime' (with its validation stats) to the buffer. + - With probability 1-alpha: + 1. Uses an external LLM, prompted with candidates from the buffer, to generate a new candidate 'a_prime'. + 2. Evaluates 'a_prime' on a validation set minibatch. + 3. Adds 'a_prime' (with its validation stats) to the buffer. + If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 0.3, + alpha: float = 0.3, + llm_model: str = None, + num_samples_in_prompt: int = 5, + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.alpha = alpha + self.llm_model = llm_model + self.num_samples_in_prompt = num_samples_in_prompt + self.llm_prompt_budget_factor = 0.5 + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + self.ucb_exploration_factor = ucb_exploration_factor + + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 + + # Initialize LLM + self.llm = LLM(model=self.llm_model) + print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], + guide, + evaluation_batch_size: int, + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params_backup = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + try: + self.optimizer.update(params_to_eval_dict) + except Exception as e: + print_color(f"Error updating agent with params_to_eval_dict: {e}. Using current agent state for eval.", "red") + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params_backup) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params_backup) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else 0 + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + if total_tracked_evaluations == 0: + total_tracked_evaluations = 1 + + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations + 1e-9) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _calculate_lcb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates Lower Confidence Bound for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('-inf') # Unvisited states get lowest bound + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + # LCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score - exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def _get_best_candidate_from_buffer(self, buffer): + """Get the best candidate from buffer, excluding those with eval_count = 0.""" + if not buffer: + return None + + # Filter out candidates with eval_count = 0 + valid_candidates = [c for c in buffer if c['eval_count'] > 0] + if not valid_candidates: + # If no candidates have been evaluated, return the one with highest UCB score + return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) + return max(valid_candidates, key=lambda c: c['score_sum'] / c['eval_count']) + + def print_intervals(self, buffer): + """Print confidence intervals for debugging in the form of open intervals (LCB, UCB)""" + print_color("Confidence intervals for all candidates:", 'cyan') + for i, candidate_entry in enumerate(buffer): + lcb = self._calculate_lcb(candidate_entry, self._total_evaluations_tracker) + ucb = candidate_entry['ucb_score'] + mean_score = candidate_entry['score_sum'] / (candidate_entry['eval_count'] or 1) + eval_count = candidate_entry['eval_count'] + + # Format as open interval (LCB, UCB) with mean score and evaluation count + interval_str = f"Action {i+1}: ({lcb:.4f}, {ucb:.4f}) [mean: {mean_score:.4f}, n: {eval_count}]" + print_color(interval_str, 'cyan') + + def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: + """ + Prompts an LLM with current buffer candidates to generate new string values for parameters. + Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. + """ + print_color("Attempting to generate candidate using LLM...", "blue") + if not self.buffer: + print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") + return None + + sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) + # Include first, last, and evenly spaced middle candidates + if len(sorted_buffer) <= self.num_samples_in_prompt: + prompt_candidates = sorted_buffer + elif self.num_samples_in_prompt <= 2: + # If only 1-2 samples requested, take first and optionally last + prompt_candidates = sorted_buffer[:self.num_samples_in_prompt] + else: + # Take first, last, and evenly spaced middle candidates + prompt_candidates = [sorted_buffer[0]] # First (highest UCB) + if self.num_samples_in_prompt > 2: + # Calculate indices for middle candidates + middle_count = self.num_samples_in_prompt - 2 # Exclude first and last + if middle_count > 0 and len(sorted_buffer) > 2: + # Evenly space middle candidates between index 1 and len-2 + middle_indices = [int(1 + i * (len(sorted_buffer) - 2) / (middle_count + 1)) + for i in range(1, middle_count + 1)] + prompt_candidates.extend([sorted_buffer[i] for i in middle_indices]) + prompt_candidates.append(sorted_buffer[-1]) # Last (lowest UCB) + + serializable_candidate_summaries = [] + for cand_entry in prompt_candidates: + summary = { + "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, + "eval_count": cand_entry['eval_count'], + "ucb_score": round(cand_entry.get('ucb_score',0), 4), + } + serializable_candidate_summaries.append(summary) + + example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} + + prompt_messages = [ + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + ] + + print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") + response_format = {"type": "json_object"} + llm_response = self.llm(prompt_messages, response_format=response_format) + llm_response_str = llm_response.choices[0].message.content + + if not llm_response_str: + print_color("LLM returned an empty response.", "red") + return None + + cleaned_llm_response_str = llm_response_str.strip() + + try: + llm_params_raw = json.loads(cleaned_llm_response_str) + except json.JSONDecodeError as e: + print_color(f"JSON parsing attempts failed: {e}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + if not isinstance(llm_params_raw, dict): + print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + candidate_params_dict = self.construct_update_dict(llm_params_raw) + return candidate_params_dict + + def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: + """Convert the suggestion in text into the right data type.""" + update_dict = {} + for node in self.agent.parameters(): + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + if type(formatted_suggestion) == str and 'def' in formatted_suggestion: + formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError) as e: + if getattr(self, 'ignore_extraction_error', False): + warnings.warn( + f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" + ) + else: + raise e + return update_dict + + def train(self, + guide, + train_dataset: Dict[str, List[Any]], + *, + num_search_iterations: int = 100, + validation_dataset: Dict[str, List[Any]] = None, + test_dataset: Dict[str, List[Any]] = None, + train_batch_size: int = 5, + evaluation_batch_size: int = 5, + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_llm_agent.pkl", + min_score_for_agent_update: Optional[float] = None, + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + print_confidence_interval: bool = True, + **kwargs + ) -> Tuple[Dict[str, Any], float]: + + if validation_dataset is None: + validation_dataset = train_dataset + if test_dataset is None: + test_dataset = train_dataset + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update + total_samples = 0 + self.total_proposals = 0 + + metrics = { + 'best_candidate_scores': [], + 'selected_action_ucb': [], + 'new_candidate_scores': [], + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + 'llm_generation_failures': 0, + 'generation_path': [] + } + + # Initial candidate evaluation + print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, + 'eval_count': initial_evals, + 'ucb_score': 0.0, + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Log initial evaluation + self.logger.log('Initial UCB score', initial_score, 0, color='blue') + self.logger.log('Total samples', total_samples, 0, color='cyan') + self.logger.log('Total proposals', self.total_proposals, 0, color='red') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + try: + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + self._update_buffer_ucb_scores() + a_prime_params_dict = None + a_prime_score = 0 + a_prime_evals = 0 + generation_method = "none" + if print_confidence_interval: + self.print_intervals(self.buffer) + + if iteration<=2 or random.random() < self.alpha: # UCB Path, for the first 2 iterations, we always use UCB because the buffer size is small, it's hard for LLM to generate good candidates + generation_method = "ucb" + metrics['generation_path'].append("ucb") + if not self.buffer: + print_color(f"Iter {iteration} (UCB Path): Buffer empty, cannot select action. Skipping.", "red") + continue + + action_candidate_a = self.select(self.buffer) + + selected_mean_score = action_candidate_a['score_sum'] / action_candidate_a['eval_count'] if action_candidate_a['eval_count'] > 0 else -np.inf + print_color(f"Iter {iteration} (UCB Path): Selected action candidate (UCB: {action_candidate_a['ucb_score']:.4f}, MeanScore: {selected_mean_score:.4f} Evals: {action_candidate_a['eval_count']})", 'blue') + # metrics['selected_action_ucb'].append(action_candidate_a['ucb_score']) + + # Log selected action UCB score + # self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') + # self.logger.log('Selected action mean score', selected_mean_score, iteration, color='cyan') + + self.optimizer.update(action_candidate_a['params']) + + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration} (UCB Path): Training minibatch empty, skipping optimizer step.", 'yellow') + continue + + total_samples += len(train_xs) + + # Forward pass for 'a' + outputs_for_a = [] + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration} (UCB): Forward for 'a'") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: + print_color(f"Iter {iteration} (UCB Path): No outputs from forward pass for 'a'. Skipping.", 'yellow') + continue + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else 0 + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) + + # Get a_prime by optimizer step + try: + returned_params = self.optimizer.step(bypassing=True, verbose=False) + if not isinstance(returned_params, dict) or not returned_params: + print_color(f"Iter {iteration} (UCB Path): Optimizer.step did not return a valid param dict for a_prime. Using current agent params.", 'yellow') + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} + else: + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in returned_params} + self.total_proposals += 1 + + except Exception as e: + print_color(f"Iter {iteration} (UCB Path): Error during optimizer.step for a_prime: {e}. Skipping.", 'red') + continue + + # Evaluate 'a' and 'a_prime' on validation set in parallel (like UCBSearchAlgorithm) + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + evaluation_results = async_run( + [self._evaluate_candidate, self._evaluate_candidate], + [ + (action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads), + (a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads) + ], + max_workers=2, + description=f"Iter {iteration} (UCB): Parallel evaluation of 'a' and 'a_prime'" + ) + (a_score, a_evals), (a_prime_score, a_prime_evals) = evaluation_results + else: + a_score, a_evals = self._evaluate_candidate( + action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads + ) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + + self._total_evaluations_tracker += a_evals + a_prime_evals + total_samples += a_evals + a_prime_evals + + # Update stats of action_candidate_a + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) + action_candidate_a['eval_count'] += len(train_xs) + self._total_evaluations_tracker += len(train_xs) + + # Update stats with validation evaluation of 'a' + action_candidate_a['score_sum'] += a_score * a_evals + action_candidate_a['eval_count'] += a_evals + + print_color(f"Iter {iteration} (UCB Path): New candidate a_prime (from UCB) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + self.logger.log('New candidate score', a_prime_score, iteration, color='green') + self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') + else: # LLM Pathcandi + generation_method = "llm" + metrics['generation_path'].append("llm") + print_color(f"Iter {iteration} (LLM Path): Generating candidate via LLM.", 'blue') + a_prime_params_dict = self._llm_generate_candidate() + + if a_prime_params_dict: + # Evaluate a_prime (from LLM path) + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += a_prime_evals + self.total_proposals += 1 + print_color(f"Iter {iteration} (LLM Path): New candidate a_prime (from LLM) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + self.logger.log('New candidate score', a_prime_score, iteration, color='green') #average new candidate score + else: + print_color(f"Iter {iteration} (LLM Path): LLM failed to generate a valid candidate. Skipping addition to buffer.", 'red') + metrics['llm_generation_failures'] += 1 + continue + + # Common logic for adding a_prime to buffer + metrics['new_candidate_scores'].append(a_prime_score) + + if a_prime_params_dict and a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, + 'eval_count': a_prime_evals, + 'ucb_score': 0.0, + 'iteration_created': iteration + } + + if len(self.buffer) == self.max_buffer_size: + self._update_buffer_ucb_scores() + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + evicted_mean_score = candidate_to_evict['score_sum'] / candidate_to_evict['eval_count'] if candidate_to_evict['eval_count'] > 0 else -np.inf + print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f}, MeanScore: {evicted_mean_score:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate (from {generation_method}) to buffer.", 'magenta') + elif a_prime_params_dict: + print_color(f"Iter {iteration}: New candidate a_prime (from {generation_method}) had invalid score/evals ({a_prime_score}, {a_prime_evals}), not added to buffer.", 'yellow') + + self._update_buffer_ucb_scores() + + # Logging + if self.buffer: + best_in_buffer = max(self.buffer, key=lambda c: (c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1))) + current_best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] if best_in_buffer['eval_count'] > 0 else 1) + metrics['best_candidate_scores'].append(current_best_score) + + valid_scores = [c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1) for c in self.buffer if c['eval_count'] > 0] + metrics['buffer_avg_score'].append(np.mean(valid_scores) if valid_scores else -np.inf) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + else: + metrics['best_candidate_scores'].append(0) + metrics['buffer_avg_score'].append(0) + metrics['buffer_avg_evals'].append(0) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], + "newly_evaluated_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_ucb_T": self._total_evaluations_tracker, + "total_samples": total_samples, + "generation_method_this_iter": generation_method, + "llm_generation_total_failures": metrics['llm_generation_failures'] + } + if generation_method == "ucb" and metrics['selected_action_ucb']: + log_data["selected_action_ucb"] = metrics['selected_action_ucb'][-1] + + # Log all important metrics + self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') + self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') + self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') + self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') + self.logger.log('Total samples', log_data['total_samples'], iteration, color='yellow') + self.logger.log('Total proposals', self.total_proposals, iteration, color='red') + + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Gen method: {generation_method}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') + + if test_dataset is not None and iteration % eval_frequency == 0: + try: + # Save current agent parameters + current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + + # Find the best candidate in the buffer (highest mean score) + best_candidate = self._get_best_candidate_from_buffer(self.buffer) + if not best_candidate: + print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') + continue + + # Load best candidate's parameters into the agent for evaluation + self.optimizer.update(best_candidate['params']) + + # Evaluate the best candidate on test set + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=self.min_score, num_threads=num_threads, + description=f"Evaluating best candidate (iteration {iteration})") + + # Restore original agent parameters + self.optimizer.update(current_params) + + self.logger.log('Test score', test_score, iteration, color='green') + except Exception as e: + print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') + + if save_frequency is not None and iteration % save_frequency == 0 and self.buffer: + try: + best_overall_candidate_entry = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) + self.optimizer.update(best_overall_candidate_entry['params']) + if hasattr(self, 'save_agent'): + self.save_agent(save_path, iteration) + best_mean_score_for_save = best_overall_candidate_entry['score_sum'] / (best_overall_candidate_entry['eval_count'] if best_overall_candidate_entry['eval_count'] > 0 else 1E-9) + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer (Mean Score: {best_mean_score_for_save:.4f}).", 'green') + else: + print_color(f"Iter {iteration}: save_agent method not found, skipping save.", 'yellow') + except Exception as e: + print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') + + except Exception as e: + print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') + self.logger.log('Iteration error', str(e), iteration, color='red') + continue + + print_color("UCB-LLM search finished.", 'blue') + + final_best_candidate = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] if final_best_candidate['eval_count'] > 0 else 1E-9) + final_best_evals = final_best_candidate['eval_count'] + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_evals}", 'green') + + self.optimizer.update(final_best_candidate['params']) + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Selects candidate with highest UCB score.''' + if not buffer: return None + return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) + + +class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): + """ + UCB Search Algorithm that uses LLM function approximation to select candidates. + """ + + def __init__(self, llm_model,num_samples_in_prompt:int=5, *args, **kwargs): + super().__init__(*args, **kwargs) + self.llm_model = llm_model + self.llm = LLM(model=self.llm_model) + self.num_samples_in_prompt = num_samples_in_prompt + print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") + + def select(self, buffer): + """Generate a new candidate entry using LLM. Note: this doesn't add it to the buffer.""" + new_action_params = self._llm_generate_candidate() + new_candidate_entry = { + 'params': new_action_params, + 'score_sum': 0, + 'eval_count': 0, + 'ucb_score': 0.0, + 'iteration_created': 0 + } + return new_candidate_entry + + def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: + """ + Prompts an LLM with current buffer candidates to generate new string values for parameters. + Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. + """ + print_color("Attempting to generate candidate using LLM...", "blue") + if not self.buffer: + print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") + return None + + sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) + # Include first, last, and evenly spaced middle candidates + if len(sorted_buffer) <= self.num_samples_in_prompt: + prompt_candidates = sorted_buffer + elif self.num_samples_in_prompt <= 2: + # If only 1-2 samples requested, take first and optionally last + prompt_candidates = sorted_buffer[:self.num_samples_in_prompt] + else: + # Take first, last, and evenly spaced middle candidates + prompt_candidates = [sorted_buffer[0]] # First (highest UCB) + if self.num_samples_in_prompt > 2: + # Calculate indices for middle candidates + middle_count = self.num_samples_in_prompt - 2 # Exclude first and last + if middle_count > 0 and len(sorted_buffer) > 2: + # Evenly space middle candidates between index 1 and len-2 + middle_indices = [int(1 + i * (len(sorted_buffer) - 2) / (middle_count + 1)) + for i in range(1, middle_count + 1)] + prompt_candidates.extend([sorted_buffer[i] for i in middle_indices]) + prompt_candidates.append(sorted_buffer[-1]) # Last (lowest UCB) + + serializable_candidate_summaries = [] + for cand_entry in prompt_candidates: + summary = { + "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, + "eval_count": cand_entry['eval_count'], + "ucb_score": round(cand_entry.get('ucb_score',0), 4), + } + serializable_candidate_summaries.append(summary) + + example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} + + prompt_messages = [ + {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, + {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} + ] + + print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") + response_format = {"type": "json_object"} + llm_response = self.llm(prompt_messages, response_format=response_format) + llm_response_str = llm_response.choices[0].message.content + + if not llm_response_str: + print_color("LLM returned an empty response.", "red") + return None + + cleaned_llm_response_str = llm_response_str.strip() + + try: + llm_params_raw = json.loads(cleaned_llm_response_str) + self.total_proposals += 1 + except json.JSONDecodeError as e: + print_color(f"JSON parsing attempts failed: {e}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + if not isinstance(llm_params_raw, dict): + print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") + print_color("Returning the candidate with the highest UCB score in the buffer.", "red") + return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] + + candidate_params_dict = self.construct_update_dict(llm_params_raw) + return candidate_params_dict + + def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: + """Convert the suggestion in text into the right data type.""" + update_dict = {} + for node in self.agent.parameters(): + if node.trainable and node.py_name in suggestion: + try: + formatted_suggestion = suggestion[node.py_name] + if type(formatted_suggestion) == str and 'def' in formatted_suggestion: + formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) + update_dict[node] = type(node.data)(formatted_suggestion) + except (ValueError, KeyError) as e: + if getattr(self, 'ignore_extraction_error', False): + warnings.warn( + f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" + ) + else: + raise e + return update_dict From 5d7320b4451db7ed6c36d02c10fb5c452fd49c0a Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 2 Jul 2025 20:07:38 +0000 Subject: [PATCH 079/172] Fix the bug in test_optoprime_udpate.py --- tests/unit_tests/test_optoprime_update.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_tests/test_optoprime_update.py b/tests/unit_tests/test_optoprime_update.py index 0b273d2f..28d185dd 100644 --- a/tests/unit_tests/test_optoprime_update.py +++ b/tests/unit_tests/test_optoprime_update.py @@ -8,6 +8,7 @@ def test_json_keys(): """ Test that the OptoPrimeV2 class correctly initializes with json_keys. """ + trace.GRAPH.clear() param = trace.node(1, trainable=True) def callable(messages, **kwargs): From a7414d1283f2f2af1b44350f7fc3dde8f6974b0b Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 2 Jul 2025 13:54:26 -0700 Subject: [PATCH 080/172] add XML format --- opto/optimizers/optoprime_v2.py | 61 ++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index e81eba2c..d4214739 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -31,38 +31,46 @@ class OptoPrimeV2(OptoPrime): In #Variables, #Inputs, #Outputs, and #Others, the format is: - + (data_type) variable_name = value - + If `(data_type)` is `code`, it means `{value}` is the source code of a python code, which may include docstring and definitions. """ ) # Optimization - default_objective = "You need to change the of the variables in #Variables to improve the output in accordance to #Feedback." + default_objective = "You need to change the `value` of the variables in #Variables to improve the output in accordance to #Feedback." output_format_prompt = dedent( """ Output_format: Your output should be in the following XML/HTML format: - - Your reasoning - + + Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't + - "suggestion": {{ - : , - : , - }} - }} - - In "reasoning", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + + variable_1_name + + new_value + ... + + + + + variable_2_name + + new_value + ... + + - If #Instruction asks for an answer, write it down in "answer". + In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. - If you need to suggest a change in the values of #Variables, write down the suggested values in "suggestion". Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + If you need to suggest a change in the values of #Variables, write down the suggested values in . Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. - If no changes or answer are needed, just output TERMINATE. + If no changes are needed, just output TERMINATE. """ ) @@ -156,9 +164,16 @@ def __init__( ) self.example_response = dedent( """ - {"reasoning": 'In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.', - "suggestion": {"a": 10} - } + + In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. + + + + a + + 10 + + """ ) @@ -176,9 +191,9 @@ def repr_node_value(node_dict): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: - temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n") + temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n") else: - temp_list.append(f"\n(code) {k}:{v[0]}\n") + temp_list.append(f"\n(code) {k}:{v[0]}\n") return "\n".join(temp_list) @staticmethod @@ -187,10 +202,10 @@ def repr_node_constraint(node_dict): for k, v in node_dict.items(): if "__code" not in k: if v[1] is not None: - temp_list.append(f"\n({type(v[0]).__name__}) {k}: {v[1]}\n") + temp_list.append(f"\n({type(v[0]).__name__}) {k}: {v[1]}\n") else: if v[1] is not None: - temp_list.append(f"\n(code) {k}: {v[1]}\n") + temp_list.append(f"\n(code) {k}: {v[1]}\n") return "\n".join(temp_list) def construct_prompt(self, summary, mask=None, *args, **kwargs): From d89707f78f086dbd1205defbb745980ffd85ac87 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 2 Jul 2025 15:00:16 -0700 Subject: [PATCH 081/172] add XML parsing --- opto/optimizers/optoprime_v2.py | 129 ++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index d4214739..cdde6643 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -12,6 +12,56 @@ from opto.optimizers.buffers import FIFOBuffer import copy +import re +from typing import Dict, Any + + +def extract_xml_like_data(text: str) -> Dict[str, Any]: + """ + Extract thinking content and improved variables from text containing XML-like tags. + + Args: + text (str): Text containing and tags + + Returns: + Dict containing: + - 'thinking': content of element + - 'variables': dict mapping variable names to their values + """ + result = { + 'thinking': '', + 'variables': {} + } + + # Extract thinking content + think_pattern = r'(.*?)' + think_match = re.search(think_pattern, text, re.DOTALL) + if think_match: + result['thinking'] = think_match.group(1).strip() + + # Extract improved variables + # Find all improved_variable blocks + var_pattern = r'(.*?)' + var_matches = re.findall(var_pattern, text, re.DOTALL) + + for var_content in var_matches: + # Extract name + name_pattern = r'(.*?)' + name_match = re.search(name_pattern, var_content, re.DOTALL) + + # Extract value + value_pattern = r'(.*?)' + value_match = re.search(value_pattern, var_content, re.DOTALL) + + if name_match and value_match: + var_name = name_match.group(1).strip() + var_value = value_match.group(1).strip() + + if var_name: # Only add if name is not empty + result['variables'][var_name] = var_value + + return result + class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( @@ -255,3 +305,82 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): self.memory.add((summary.variables, summary.user_feedback)) return system_prompt, user_prompt + + def extract_llm_suggestion(self, response: str): + """Extract the suggestion from the response.""" + + suggestion = extract_xml_like_data(response) + + # attempt_n = 0 + # while attempt_n < 2: + # try: + # suggestion = json.loads(response)["suggestion"] + # break + # except json.JSONDecodeError: + # # Remove things outside the brackets + # response = re.findall(r"{.*}", response, re.DOTALL) + # if len(response) > 0: + # response = response[0] + # attempt_n += 1 + # except Exception: + # attempt_n += 1 + + # if not isinstance(suggestion, dict): + # suggestion = {} + # + # if len(suggestion) == 0: + # # we try to extract key/value separately and return it as a dictionary + # pattern = r'"suggestion"\s*:\s*\{(.*?)\}' + # suggestion_match = re.search(pattern, str(response), re.DOTALL) + # if suggestion_match: + # suggestion = {} + # # Extract the entire content of the suggestion dictionary + # suggestion_content = suggestion_match.group(1) + # # Regex to extract each key-value pair; + # # This scheme assumes double quotes but is robust to missing commas at the end of the line + # pair_pattern = r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"' + # # Find all matches of key-value pairs + # pairs = re.findall(pair_pattern, suggestion_content, re.DOTALL) + # for key, value in pairs: + # suggestion[key] = value + + if len(suggestion) == 0: + if not self.ignore_extraction_error: + print("Cannot extract suggestion from LLM's response:") + print(response) + + # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) + # then we remove such suggestion + keys_to_remove = [] + for key, value in suggestion.items(): + if "__code" in key and value.strip() == "": + keys_to_remove.append(key) + for key in keys_to_remove: + del suggestion[key] + + return suggestion + + def call_llm( + self, + system_prompt: str, + user_prompt: str, + verbose: Union[bool, str] = False, + max_tokens: int = 4096, + ): + """Call the LLM with a prompt and return the response.""" + if verbose not in (False, "output"): + print("Prompt\n", system_prompt + user_prompt) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + + response = self.llm(messages=messages, max_tokens=max_tokens) + + response = response.choices[0].message.content + + if verbose: + print("LLM response:\n", response) + return response + From 3e6cec2a01641141f5942c90dddb5ac0a6febf68 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 3 Jul 2025 19:58:12 +0000 Subject: [PATCH 082/172] Move copy def to ParameterContainer and update it to support nesting. --- opto/trace/containers.py | 27 +++++++++++- opto/trace/modules.py | 20 +++------ tests/unit_tests/test_modules.py | 76 ++++++++++++++++---------------- 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/opto/trace/containers.py b/opto/trace/containers.py index a216118d..402e39c8 100644 --- a/opto/trace/containers.py +++ b/opto/trace/containers.py @@ -2,6 +2,7 @@ from collections import UserDict, UserList from opto.trace.nodes import ParameterNode import functools +import copy class NodeContainer: @@ -49,7 +50,7 @@ def parameters_dict(self): method = attr.func.__self__ if trainable_method(method): parameters[name] = method.parameter - if trainable_method(attr): # method attribute + elif trainable_method(attr): # method attribute parameters[name] = attr.parameter elif isinstance(attr, ParameterNode): parameters[name] = attr @@ -63,6 +64,30 @@ def parameters_dict(self): return parameters # include both trainable and non-trainable parameters + def copy(self): + """Return a deep copy of the ParameterContainer except for the parameters + are set to the originals.""" + + # NOTE This current code is not optimized for speed; it does extra traversals and copying. + + new_container = copy.deepcopy(self) + + # Set the parameters to the original ones + for name, attr in inspect.getmembers(self): + if isinstance(attr, functools.partial): # this is a class method + method = attr.func.__self__ + if trainable_method(method): + new_attr = getattr(new_container, name) + setattr(new_attr.func.__self__, 'parameter', method.parameter) + elif trainable_method(attr): # method attribute + new_attr = getattr(new_container, name) + new_attr.parameter = attr.parameter + elif isinstance(attr, ParameterNode): + setattr(new_container, name, attr) + elif isinstance(attr, ParameterContainer): + setattr(new_container, name, attr.copy()) # recursion + + return new_container class Seq(UserList, ParameterContainer): """ diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 6b7f0114..9310c2ff 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -16,6 +16,7 @@ def model(cls): """ class ModelWrapper(cls, Module): + def model_dump(self, filename, projections: Optional[List[Projection]] = None): """Dump the model's source code to a file, including all methods and attributes. Ignores dunder methods unless they were overridden by the user. @@ -24,7 +25,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): projections = [BlackCodeFormatter()] trace_model_body = f"class {cls.__name__}:\n" - + # Get all members of the class all_members = inspect.getmembers(self) cls_members = inspect.getmembers(cls) @@ -39,7 +40,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): if name not in cls_member_names: continue - + # Include if it's not a dunder method or if it was overridden if not name.startswith('__'): filtered_members.append((name, member)) @@ -72,7 +73,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): source = textwrap.dedent(source) indented = textwrap.indent(source, " ") trace_model_body += indented - + if i < len(all_members) - 1: trace_model_body += "\n" # only one newline between members @@ -80,7 +81,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): # WARNING: there might be corner cases that this static analysis does not cover import re node_pattern = r'self\.(\w+)\s*=\s*node\([^)]*\)' - + def replace_node(match): attr_name = match.group(1) if hasattr(self, attr_name): @@ -88,7 +89,7 @@ def replace_node(match): if hasattr(attr, 'data'): return f"self.{attr_name} = {attr.data}" return match.group(0) # Return original if replacement not possible - + trace_model_body = re.sub(node_pattern, replace_node, trace_model_body) trace_model_body = functools.reduce(lambda body, proj: proj.project(body), projections, trace_model_body) @@ -107,15 +108,6 @@ def forward(self, *args, **kwargs): def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) - - def copy(self): - """Return a deep copy of the module except for the parameters - are set to the originals.""" - new_module = copy.deepcopy(self) - for k, v in self.parameters_dict().items(): - if hasattr(new_module, k): - setattr(new_module, k, v) - return new_module def save(self, file_name: str): """Save the parameters of the model to a pickle file.""" diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index ae1e9267..a1bbc17f 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -348,62 +348,62 @@ def __init__(self): super().__init__() self.offset = node(2, trainable=True) self.multiplier = node(1.5, trainable=True) - + @bundle(trainable=True) def add(self, x, y): """Add two numbers with an offset""" return x + y + self.offset - + @bundle(trainable=True) def multiply(self, x, y): """Multiply two numbers with a multiplier""" return x * y * self.multiplier - + # Create instance and modify parameters calc = StrangeCalculator() calc.offset._data = 3 calc.multiplier._data = 2.0 calc.add.parameter._data = "def add(self, x, y):\n return x + y + self.offset + 1" calc.multiply.parameter._data = "def multiply(self, x, y):\n return x * y * self.multiplier * 2" - + # Dump the model temp_file = "temp_calculator.py" try: calc.model_dump(temp_file) - + # Import the dumped class import importlib.util spec = importlib.util.spec_from_file_location("temp_calculator", temp_file) temp_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(temp_module) - + # Get the imported class ImportedCalculator = temp_module.StrangeCalculator - + # Create instance and test functionality imported_calc = ImportedCalculator() - + # Test the modified behavior result_add = imported_calc.add(5, 3) result_multiply = imported_calc.multiply(4, 2) - + # Verify the results match our expected modified behavior # add: 5 + 3 + 3 + 1 = 12 # multiply: 4 * 2 * 2.0 * 2 = 32 assert result_add == 12, f"Expected 12, got {result_add}" assert result_multiply == 32, f"Expected 32, got {result_multiply}" - + # Verify the attributes have the correct values assert imported_calc.offset == 3 assert imported_calc.multiplier == 2.0 - + finally: if os.path.exists(temp_file): os.remove(temp_file) def test_copy_function(): """Test the copy function of Module class.""" - + @model class TestCopyClass: def __init__(self): @@ -412,76 +412,76 @@ def __init__(self): self.regular_attr = "original_value" self.list_attr = [1, 2, 3] self.dict_attr = {"key": "value"} - + @bundle(trainable=True) def test_method(self, x): return x + self._param - + def forward(self, x): return self.test_method(x) - + # Create original instance original = TestCopyClass() original.regular_attr = "modified_value" original.list_attr.append(4) original.dict_attr["new_key"] = "new_value" - + # Create a copy copied = original.copy() - + # Test that it's a different object assert copied is not original - + # Test that regular attributes are copied (deep copy) assert copied.regular_attr == "modified_value" assert copied.list_attr == [1, 2, 3, 4] assert copied.dict_attr == {"key": "value", "new_key": "new_value"} - + # Test that parameters are references to the original parameters assert copied._param is original._param assert copied.test_method.parameter is original.test_method.parameter - + # Test that modifying the original parameter affects the copy original._param._data = 20 assert copied._param._data == 20 - + # Test that modifying the copy's parameter affects the original copied._param._data = 30 assert original._param._data == 30 - + # Test that the copy can still function result = copied.forward(5) assert result._data == 35 # 5 + 30 - + # Test that modifying regular attributes doesn't affect the original copied.regular_attr = "copy_only_value" assert original.regular_attr == "modified_value" - + # Test that modifying list/dict attributes doesn't affect the original (deep copy) copied.list_attr.append(5) assert len(original.list_attr) == 4 assert len(copied.list_attr) == 5 - + copied.dict_attr["copy_only"] = "copy_value" assert "copy_only" not in original.dict_attr assert "copy_only" in copied.dict_attr def test_copy_function_with_nested_modules(): """Test the copy function with nested modules.""" - + @model class NestedModule: def __init__(self): super().__init__() self._nested_param = node(5, trainable=True) - + @bundle(trainable=True) def nested_method(self, x): return x * self._nested_param - + def forward(self, x): return self.nested_method(x) - + @model class ParentModule: def __init__(self): @@ -489,37 +489,37 @@ def __init__(self): self._param = node(10, trainable=True) self._nested = NestedModule() self.regular_attr = "parent_value" - + @bundle(trainable=True) def parent_method(self, x): return self._nested.forward(x) + self._param - + def forward(self, x): return self.parent_method(x) - + # Create original instance original = ParentModule() original.regular_attr = "modified_parent" original._nested._nested_param._data = 7 - + # Create a copy copied = ParentModule() copied = original.copy() - + # Test that it's a different object assert copied is not original - + # Test that nested module is copied but parameters are references assert copied._nested is not original._nested # Different object assert copied._nested._nested_param is original._nested._nested_param # Same parameter reference - + # Test that regular attributes are copied assert copied.regular_attr == "modified_parent" - + # Test that modifying nested parameter affects both original._nested._nested_param._data = 8 assert copied._nested._nested_param._data == 8 - + # Test that the copy can still function result = copied.forward(3) assert result._data == 34 # (3 * 8) + 10 From 83a5220cdcfeecddc42176c0a7d3676d1092cec2 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 8 Jul 2025 22:13:00 +0000 Subject: [PATCH 083/172] Make DataLoader an iterator and add a sample method for continuously sampling. --- opto/trainer/loader.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/opto/trainer/loader.py b/opto/trainer/loader.py index e61532b7..90d738f9 100644 --- a/opto/trainer/loader.py +++ b/opto/trainer/loader.py @@ -23,17 +23,32 @@ def __init__(self, dataset, batch_size=1, replacement=False, shuffle=True): self.replacement = replacement self.shuffle = shuffle self._indices = self._update_indices() + self._i = 0 def __iter__(self): - indices = self._indices - for i in range(0, len(indices), self.batch_size): - xs = [ self.dataset['inputs'][ind] for ind in indices[i:i + self.batch_size] ] - infos = [self.dataset['infos'][ind] for ind in indices[i:i + self.batch_size] ] - yield xs, infos - - if self.shuffle: - self._indices = self._update_indices() + return self + + def __next__(self): + """ Get the next batch of data """ + if self._i >= len(self._indices): + if self.shuffle: + self._indices = self._update_indices() + self._i = 0 + raise StopIteration + indices = self._indices[self._i: min(self._i + self.batch_size, len(self._indices))] + xs = [self.dataset['inputs'][ind] for ind in indices] + infos = [self.dataset['infos'][ind] for ind in indices] + self._i += self.batch_size + return xs, infos def _update_indices(self): N = len(self.dataset['inputs']) return np.random.choice(N, size=N, replace=self.replacement) + + def sample(self): + """ Sample a batch of data from the dataset """ + try: + xs, infos = next(self) + return xs, infos + except StopIteration: + return self.sample() From 8bdc0a1c04a84105a8e89de9f6880d4721365eca Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 8 Jul 2025 22:25:29 +0000 Subject: [PATCH 084/172] Add test_dataloader.py --- tests/unit_tests/test_dataloader.py | 91 +++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/unit_tests/test_dataloader.py diff --git a/tests/unit_tests/test_dataloader.py b/tests/unit_tests/test_dataloader.py new file mode 100644 index 00000000..8d4db810 --- /dev/null +++ b/tests/unit_tests/test_dataloader.py @@ -0,0 +1,91 @@ +from opto.trainer.loader import DataLoader + + + +def run_for_loop(dataloader): + print('Running for-loop') + for i, (inputs, infos) in enumerate(dataloader): + + print(f"Inputs: {inputs}, Infos: {infos}") + + if i == 0: + assert inputs == [1, 2], f"First batch should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"First batch should contain infos 'a' and 'b'. Get: {infos}" + elif i == 1: + assert inputs == [3, 4], f"Second batch should contain inputs 3 and 4. Get: {inputs}" + assert infos == ['c', 'd'], f"Second batch should contain infos 'c' and 'd'. Get: {infos}" + elif i == 2: + assert inputs == [5], f"Third batch should contain input 5. Get: {inputs}" + assert infos == ['e'], f"Third batch should contain info 'e'. Get: {infos}" + +def run_next(dataloader): + inputs, infos = next(dataloader) + print('Running next()') + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [1, 2], f"First batch should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"First batch should contain infos 'a' and 'b'. Get: {infos}" + + inputs, infos = next(dataloader) + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [3, 4], f"Second batch should contain inputs 3 and 4. Get: {inputs}" + assert infos == ['c', 'd'], f"Second batch should contain infos 'c' and 'd'. Get: {infos}" + + inputs, infos = next(dataloader) + print(f"Inputs: {inputs}, Infos: {infos}") + + assert inputs == [5], f"Third batch should contain input 5. Get: {inputs}" + assert infos == ['e'], f"Third batch should contain info 'e'. Get: {infos}" + + try: + next(dataloader) + except StopIteration: + print("No more data to iterate over, as expected.") + +def run_sample(dataloader): + + print('Running sample()') + inputs, infos = dataloader.sample() + assert inputs == [1, 2], f"First sample should contain inputs 1 and 2. Get: {inputs}" + assert infos == ['a', 'b'], f"First sample should contain infos 'a' and 'b'. Get: {infos}" + inputs, infos = dataloader.sample() + assert inputs == [3, 4], f"Second sample should contain inputs 3 and 4. Get: {inputs}" + assert infos == ['c', 'd'], f"Second sample should contain infos 'c' and 'd'. Get: {infos}" + inputs, infos = dataloader.sample() + assert inputs == [5], f"Third sample should contain input 5. Get: {inputs}" + assert infos == ['e'], f"Third sample should contain info 'e'. Get: {infos}" + + # At this point, the dataloader should be reset. No need to catch StopIteration when calling sample again + +def test_dataloader(): + + dataset = { + 'inputs': [1, 2, 3, 4, 5], + 'infos': ['a', 'b', 'c', 'd', 'e'] + } + dataloader = DataLoader(dataset, batch_size=2, randomize=False) + + # Test for-loop usage + run_for_loop(dataloader) + run_for_loop(dataloader) # make sure it can be iterated multiple times + + # Test next() usage + run_next(dataloader) + run_next(dataloader) # make sure it can be called multiple times + + # Test sample() method + run_sample(dataloader) + run_sample(dataloader) # make sure it can be called multiple times + + # Test for-loop usage + run_for_loop(dataloader) + run_for_loop(dataloader) # make sure it can be iterated multiple times + + # Test next() usage + run_next(dataloader) + run_next(dataloader) # make sure it can be called multiple times + + # Test sample() method + run_sample(dataloader) + run_sample(dataloader) # make sure it can be called multiple times \ No newline at end of file From 5f369367a987e8bc0549239faeb294a2814c739f Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Wed, 9 Jul 2025 14:32:29 -0500 Subject: [PATCH 085/172] fix a bug in AutoGuide --- opto/trainer/guide.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index cad39f37..8a727d86 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -45,7 +45,7 @@ def metric(self, query: str, response: str, reference: Optional[str] = None, **k """ Exact match metric """ return self.get_feedback(query, response, reference)[0] - def copy(): + def copy(self): """ Create a copy of the guide instance. Returns: From 94ad0e507c0ab2923c1531962641c2085719142c Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Wed, 9 Jul 2025 14:47:57 -0500 Subject: [PATCH 086/172] fix a bug in batch_run --- opto/trainer/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index 16067b78..6f7ccc15 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -113,8 +113,8 @@ def _fun(*args, **kwargs): # deepcopy if it is a trace.Module (as they may have mutable state) # Module.copy() is used to create a new instance with the same parameters - _args = [arg.copy() if isinstance(arg, (Module, AutoGuide)) else arg for arg in args] - _kwargs = {k: v.copy() if isinstance(v, (Module, AutoGuide)) else v for k, v in kwargs.items()} + _args = [[a.copy() if isinstance(a, (Module, AutoGuide)) else a for a in arg ] for arg in args ] + _kwargs = {k: [a.copy() if isinstance(a, (Module, AutoGuide)) else a for a in v ] for k, v in kwargs.items() } # Run the forward function in parallel using asyncio with the same parameters. # Since trace.Node is treated as immutable, we can safely use the same instance. From b2bafad94a83ddebc6cced37bd871e49f83138cc Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 15:50:29 -0400 Subject: [PATCH 087/172] adding working XML parsing and new format --- opto/optimizers/optoprime_v2.py | 61 +++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index cdde6643..023cfffa 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -25,19 +25,19 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: Returns: Dict containing: - - 'thinking': content of element + - 'reasoning': content of element - 'variables': dict mapping variable names to their values """ result = { - 'thinking': '', + 'reasoning': '', 'variables': {} } # Extract thinking content - think_pattern = r'(.*?)' + think_pattern = r'(.*?)' think_match = re.search(think_pattern, text, re.DOTALL) if think_match: - result['thinking'] = think_match.group(1).strip() + result['reasoning'] = think_match.group(1).strip() # Extract improved variables # Find all improved_variable blocks @@ -62,7 +62,35 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: return result +# TODO: solution1 -> solution2 -> solution3 +# TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 +# TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases + class OptoPrimeV2(OptoPrime): + # TODO: 1. merge variable and constraint + # TODO: 2. Compact representation: some node is very long to describe in text, show a truncated version (long list of data) + # TODO: if the node displaying, if the string description is too long, we should have a limit on character we send to LLM, display truncated format + # TODO: (a flag to set it) + # TODO: LLM has the option to check the value of truncated one + # TODO: turn into a conversation round + # TODO: and show in a separate message + # TODO: 3. Compact representation (compress function) + # TODO: batchify, list of inputs, output is a list of inputs + # TODO: information is redundant + # TODO: idea 1: for each operator, we can identify repeated structure + # TODO: idea 2: for each bundle/op, the user can pass in a callable function, take original output, return a string + # TODO: idea 2-2: each node has a string representation of data, that's what the optimizer should use (this string is fixed) + # TODO: some are too redundant to describe + # TODO: x = a + b + # TODO: y = a + c + # TODO: z = f(x, y) => z = f(a+b, a+c) + # TODO: z = g(a, b, c) + + # TODO: Node level change: format_data_repr(func: Callable[[Node], str]) -> None + # TODO: Check format data representation + # TODO: input would be the data of this node, return would be a string + # TODO: later on optimizer just calls this + # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( """ @@ -92,13 +120,14 @@ class OptoPrimeV2(OptoPrime): # Optimization default_objective = "You need to change the `value` of the variables in #Variables to improve the output in accordance to #Feedback." - output_format_prompt = dedent( + output_format_prompt_template = dedent( """ Output_format: Your output should be in the following XML/HTML format: - - Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't - + ``` + + Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't want to change it. + variable_1_name @@ -115,6 +144,7 @@ class OptoPrimeV2(OptoPrime): ... + ``` In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. @@ -169,6 +199,8 @@ class OptoPrimeV2(OptoPrime): """ ) + # TODO: add an option to replace XML tags if needed by user + default_prompt_symbols = { "variables": "#Variables", "constraints": "#Constraints", @@ -204,19 +236,19 @@ def __init__( instruction=self.default_objective, code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", documentation="add: add x and y \nsubtract: subtract y from x", - variables="(int) a = 5", + variables="\n(int) a = 5\n", constraints="a: a > 0", - outputs="(int) z = 1", - others="(int) y = 6", - inputs="(int) b = 1\n(int) c = 5", + outputs="\n(int) z = 1\n", + others="\n(int) y = 6\n", + inputs="\n(int) b = 1\n(int) c = 5\n", feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", stepsize=1, ) self.example_response = dedent( """ - + In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. - + a @@ -226,6 +258,7 @@ def __init__( """ ) + self.output_format_prompt = self.output_format_prompt_template self.include_example = include_example self.max_tokens = max_tokens From 6d26d87e70eef5562b680f2954030f86f42b81ff Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 16:39:11 -0400 Subject: [PATCH 088/172] separate node into two types --- opto/optimizers/optoprime_v2.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 023cfffa..29a58cf8 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -109,8 +109,21 @@ class OptoPrimeV2(OptoPrime): In #Variables, #Inputs, #Outputs, and #Others, the format is: + For primitive variables (int, float, list, etc.), we express as this: - (data_type) variable_name = value + (data_type) variable_name = value + constraint_expression + + + For functions or code variables, we express as this: + + (data_type) variable_name + + value + + + constraint_expression + If `(data_type)` is `code`, it means `{value}` is the source code of a python code, which may include docstring and definitions. @@ -232,6 +245,17 @@ def __init__( self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() self.objective = objective or self.default_objective + """ + + (data_type) variable_name + + value + + + constraint_expression + + + """ self.example_problem = ProblemInstance.problem_template.format( instruction=self.default_objective, code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", @@ -240,7 +264,7 @@ def __init__( constraints="a: a > 0", outputs="\n(int) z = 1\n", others="\n(int) y = 6\n", - inputs="\n(int) b = 1\n(int) c = 5\n", + inputs="\n(int) b = 1\n\n\n(int) c = 5\n", feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", stepsize=1, ) From e8a1aee260cb3bfbf94f5ce07fde9e33de5be4e0 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 17:43:23 -0400 Subject: [PATCH 089/172] constraint is integrated into the variable now --- opto/optimizers/optoprime_v2.py | 139 ++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 50 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 29a58cf8..0b648e9c 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -2,7 +2,7 @@ from typing import Any, List, Dict, Union, Tuple from textwrap import dedent, indent from dataclasses import dataclass, asdict -from opto.optimizers.optoprime import OptoPrime, ProblemInstance +from opto.optimizers.optoprime import OptoPrime from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator @@ -15,6 +15,58 @@ import re from typing import Dict, Any +@dataclass +class ProblemInstance: + instruction: str + code: str + documentation: str + variables: str + inputs: str + others: str + outputs: str + feedback: str + + problem_template = dedent( + """ + #Instruction + {instruction} + + #Code + {code} + + #Documentation + {documentation} + + #Variables + {variables} + + #Inputs + {inputs} + + #Others + {others} + + #Outputs + {outputs} + + #Feedback + {feedback} + """ + ) + + def __repr__(self) -> str: + return self.problem_template.format( + instruction=self.instruction, + code=self.code, + documentation=self.documentation, + variables=self.variables, + inputs=self.inputs, + outputs=self.outputs, + others=self.others, + feedback=self.feedback, + ) + + def extract_xml_like_data(text: str) -> Dict[str, Any]: """ @@ -101,7 +153,6 @@ class OptoPrimeV2(OptoPrime): - #Code: the code defined in the problem. - #Documentation: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. - #Variables: the input variables that you can change. - - #Constraints: the constraints or descriptions of the variables in #Variables. - #Inputs: the values of other inputs to the code, which are not changeable. - #Others: the intermediate values created through the code execution. - #Outputs: the result of the code output. @@ -216,7 +267,6 @@ class OptoPrimeV2(OptoPrime): default_prompt_symbols = { "variables": "#Variables", - "constraints": "#Constraints", "inputs": "#Inputs", "outputs": "#Outputs", "others": "#Others", @@ -298,21 +348,11 @@ def repr_node_value(node_dict): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: - temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n") - else: - temp_list.append(f"\n(code) {k}:{v[0]}\n") - return "\n".join(temp_list) - - @staticmethod - def repr_node_constraint(node_dict): - temp_list = [] - for k, v in node_dict.items(): - if "__code" not in k: - if v[1] is not None: - temp_list.append(f"\n({type(v[0]).__name__}) {k}: {v[1]}\n") + constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " + temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n{constraint_expr}\n\n") else: - if v[1] is not None: - temp_list.append(f"\n(code) {k}: {v[1]}\n") + constraint_expr = f"\n(code) {k}: {v[1]}\n" + temp_list.append(f"\n(code) {k}\n\n{v[0]}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) def construct_prompt(self, summary, mask=None, *args, **kwargs): @@ -363,44 +403,43 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): return system_prompt, user_prompt + def problem_instance(self, summary, mask=None): + mask = mask or [] + return ProblemInstance( + instruction=self.objective if "#Instruction" not in mask else "", + code=( + "\n".join([v for k, v in sorted(summary.graph)]) + if "#Code" not in mask + else "" + ), + documentation=( + "\n".join([f"[{k}] {v}" for k, v in summary.documentation.items()]) + if "#Documentation" not in mask + else "" + ), + variables=( + self.repr_node_value(summary.variables) + if "#Variables" not in mask + else "" + ), + inputs=( + self.repr_node_value(summary.inputs) if "#Inputs" not in mask else "" + ), + outputs=( + self.repr_node_value(summary.output) if "#Outputs" not in mask else "" + ), + others=( + self.repr_node_value(summary.others) if "#Others" not in mask else "" + ), + feedback=summary.user_feedback if "#Feedback" not in mask else "", + ) + + def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" suggestion = extract_xml_like_data(response) - # attempt_n = 0 - # while attempt_n < 2: - # try: - # suggestion = json.loads(response)["suggestion"] - # break - # except json.JSONDecodeError: - # # Remove things outside the brackets - # response = re.findall(r"{.*}", response, re.DOTALL) - # if len(response) > 0: - # response = response[0] - # attempt_n += 1 - # except Exception: - # attempt_n += 1 - - # if not isinstance(suggestion, dict): - # suggestion = {} - # - # if len(suggestion) == 0: - # # we try to extract key/value separately and return it as a dictionary - # pattern = r'"suggestion"\s*:\s*\{(.*?)\}' - # suggestion_match = re.search(pattern, str(response), re.DOTALL) - # if suggestion_match: - # suggestion = {} - # # Extract the entire content of the suggestion dictionary - # suggestion_content = suggestion_match.group(1) - # # Regex to extract each key-value pair; - # # This scheme assumes double quotes but is robust to missing commas at the end of the line - # pair_pattern = r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"' - # # Find all matches of key-value pairs - # pairs = re.findall(pair_pattern, suggestion_content, re.DOTALL) - # for key, value in pairs: - # suggestion[key] = value - if len(suggestion) == 0: if not self.ignore_extraction_error: print("Cannot extract suggestion from LLM's response:") From f33f5758dda186e315bbec43429f0c4d27e8e036 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 18:05:15 -0400 Subject: [PATCH 090/172] add expression truncation --- opto/optimizers/optoprime_v2.py | 35 ++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 0b648e9c..89930ccb 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -119,7 +119,7 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: # TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases class OptoPrimeV2(OptoPrime): - # TODO: 1. merge variable and constraint + # TODO: 1. merge variable and constraint (DONE) # TODO: 2. Compact representation: some node is very long to describe in text, show a truncated version (long list of data) # TODO: if the node displaying, if the string description is too long, we should have a limit on character we send to LLM, display truncated format # TODO: (a flag to set it) @@ -289,6 +289,7 @@ def __init__( max_tokens=4096, log=True, prompt_symbols=None, + initial_var_char_limit=100, **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) @@ -333,6 +334,7 @@ def __init__( """ ) self.output_format_prompt = self.output_format_prompt_template + self.initial_var_char_limit = initial_var_char_limit self.include_example = include_example self.max_tokens = max_tokens @@ -355,6 +357,29 @@ def repr_node_value(node_dict): temp_list.append(f"\n(code) {k}\n\n{v[0]}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) + def repr_node_value_compact(self, node_dict): + temp_list = [] + for k, v in node_dict.items(): + if "__code" not in k: + constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " + # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr + # node_value = str(v[0])[:self.initial_var_char_limit] + node_value = self.truncate_expression(v[0], self.initial_var_char_limit) + temp_list.append(f"\n({type(v[0]).__name__}) {k}={node_value}\n{constraint_expr}\n\n") + else: + constraint_expr = f"\n(code) {k}: {v[1]}\n" + # node_value = str(v[0])[:self.initial_var_char_limit] + node_value = self.truncate_expression(v[0], self.initial_var_char_limit) + temp_list.append( + f"\n(code) {k}\n\n{node_value}\n\n{constraint_expr}\n\n") + return "\n".join(temp_list) + + def truncate_expression(self, value, limit): + value = str(value) + if len(value) > limit: + return value[:limit] + "...(skipped due to length limit)" + return value + def construct_prompt(self, summary, mask=None, *args, **kwargs): """Construct the system and user prompt.""" system_prompt = ( @@ -418,18 +443,18 @@ def problem_instance(self, summary, mask=None): else "" ), variables=( - self.repr_node_value(summary.variables) + self.repr_node_value_compact(summary.variables) if "#Variables" not in mask else "" ), inputs=( - self.repr_node_value(summary.inputs) if "#Inputs" not in mask else "" + self.repr_node_value_compact(summary.inputs) if "#Inputs" not in mask else "" ), outputs=( - self.repr_node_value(summary.output) if "#Outputs" not in mask else "" + self.repr_node_value_compact(summary.output) if "#Outputs" not in mask else "" ), others=( - self.repr_node_value(summary.others) if "#Others" not in mask else "" + self.repr_node_value_compact(summary.others) if "#Others" not in mask else "" ), feedback=summary.user_feedback if "#Feedback" not in mask else "", ) From bf55724233e26052dcd240fdc56dcc600c183077 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 23:32:29 -0400 Subject: [PATCH 091/172] update XML tag --- opto/optimizers/optoprime_v2.py | 155 ++++++++++++++------------------ 1 file changed, 69 insertions(+), 86 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 89930ccb..c43584d2 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -28,28 +28,28 @@ class ProblemInstance: problem_template = dedent( """ - #Instruction + # Instruction {instruction} - #Code + # Code {code} - #Documentation + # Documentation {documentation} - #Variables + # Variables {variables} - #Inputs + # Inputs {inputs} - #Others + # Others {others} - #Outputs + # Outputs {outputs} - #Feedback + # Feedback {feedback} """ ) @@ -66,8 +66,6 @@ def __repr__(self) -> str: feedback=self.feedback, ) - - def extract_xml_like_data(text: str) -> Dict[str, Any]: """ Extract thinking content and improved variables from text containing XML-like tags. @@ -93,7 +91,7 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: # Extract improved variables # Find all improved_variable blocks - var_pattern = r'(.*?)' + var_pattern = r'(.*?)' var_matches = re.findall(var_pattern, text, re.DOTALL) for var_content in var_matches: @@ -158,25 +156,18 @@ class OptoPrimeV2(OptoPrime): - #Outputs: the result of the code output. - #Feedback: the feedback about the code's execution result. - In #Variables, #Inputs, #Outputs, and #Others, the format is: + In `#Variables`, `#Inputs`, `#Outputs`, and `#Others`, the format is: - For primitive variables (int, float, list, etc.), we express as this: - - (data_type) variable_name = value - constraint_expression + For variables we express as this: + + + value + + + constraint_expression + - For functions or code variables, we express as this: - - (data_type) variable_name - - value - - - constraint_expression - - - If `(data_type)` is `code`, it means `{value}` is the source code of a python code, which may include docstring and definitions. """ ) @@ -193,24 +184,24 @@ class OptoPrimeV2(OptoPrime): Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't want to change it. - - variable_1_name - - new_value - ... - - + + variable_1_name + + new_value + ... + + - - variable_2_name - - new_value - ... - - + + variable_2_name + + new_value + ... + + ``` - In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. If you need to suggest a change in the values of #Variables, write down the suggested values in . Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. @@ -266,14 +257,14 @@ class OptoPrimeV2(OptoPrime): # TODO: add an option to replace XML tags if needed by user default_prompt_symbols = { - "variables": "#Variables", - "inputs": "#Inputs", - "outputs": "#Outputs", - "others": "#Others", - "feedback": "#Feedback", - "instruction": "#Instruction", - "code": "#Code", - "documentation": "#Documentation", + "variables": "# Variables", + "inputs": "# Inputs", + "outputs": "# Outputs", + "others": "# Others", + "feedback": "# Feedback", + "instruction": "# Instruction", + "code": "# Code", + "documentation": "# Documentation", } def __init__( @@ -296,26 +287,15 @@ def __init__( self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() self.objective = objective or self.default_objective - """ - - (data_type) variable_name - - value - - - constraint_expression - - - """ self.example_problem = ProblemInstance.problem_template.format( instruction=self.default_objective, code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", documentation="add: add x and y \nsubtract: subtract y from x", - variables="\n(int) a = 5\n", - constraints="a: a > 0", - outputs="\n(int) z = 1\n", - others="\n(int) y = 6\n", - inputs="\n(int) b = 1\n\n\n(int) c = 5\n", + variables="""\n\n5\n\n\na: a > 0\n\n""", + # constraints="a: a > 0", + outputs="""\n\n1\n\n""", + others="""\n\n6\n\n""", + inputs="""\n\n1\n\n\n\n\n5\n\n""", feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", stepsize=1, ) @@ -325,12 +305,12 @@ def __init__( In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. - - a - - 10 - - + + a + + 10 + + """ ) self.output_format_prompt = self.output_format_prompt_template @@ -351,30 +331,33 @@ def repr_node_value(node_dict): for k, v in node_dict.items(): if "__code" not in k: constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " - temp_list.append(f"\n({type(v[0]).__name__}) {k}={v[0]}\n{constraint_expr}\n\n") + temp_list.append(f"\n{v[0]}\n{constraint_expr}\n\n") else: - constraint_expr = f"\n(code) {k}: {v[1]}\n" - temp_list.append(f"\n(code) {k}\n\n{v[0]}\n\n{constraint_expr}\n\n") + constraint_expr = f"\n{v[1]}\n" + temp_list.append(f"\n\n{v[0]}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) - def repr_node_value_compact(self, node_dict): + def repr_node_value_compact(self, node_dict, xml_root_tag="node"): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: - constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " - # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr - # node_value = str(v[0])[:self.initial_var_char_limit] node_value = self.truncate_expression(v[0], self.initial_var_char_limit) - temp_list.append(f"\n({type(v[0]).__name__}) {k}={node_value}\n{constraint_expr}\n\n") + if v[1] is not None: + constraint_expr = f"\n{v[1]}\n" + temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n\n{node_value}\n\n{constraint_expr}\n\n") + else: + temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n\n{node_value}\n\n\n") else: - constraint_expr = f"\n(code) {k}: {v[1]}\n" - # node_value = str(v[0])[:self.initial_var_char_limit] - node_value = self.truncate_expression(v[0], self.initial_var_char_limit) - temp_list.append( - f"\n(code) {k}\n\n{node_value}\n\n{constraint_expr}\n\n") + constraint_expr = f"\n{v[1]}\n" + # we only truncate the function body + signature = v[1].replace("The code should start with:\n", "") + func_body = v[0].replace(signature, "") + node_value = self.truncate_expression(func_body, self.initial_var_char_limit) + temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"code\">\n\n{signature}{node_value}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) def truncate_expression(self, value, limit): + # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr value = str(value) if len(value) > limit: return value[:limit] + "...(skipped due to length limit)" @@ -443,7 +426,7 @@ def problem_instance(self, summary, mask=None): else "" ), variables=( - self.repr_node_value_compact(summary.variables) + self.repr_node_value_compact(summary.variables, xml_root_tag="variable") if "#Variables" not in mask else "" ), From 88d7328757cac72aa40040a1c2f2eba8d1ca5a13 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 23:44:30 -0400 Subject: [PATCH 092/172] add robust XML parsing --- opto/optimizers/optoprime_v2.py | 116 ++++-- .../unit_tests/test_optimizer_xml_parsing.py | 336 ++++++++++++++++++ 2 files changed, 421 insertions(+), 31 deletions(-) create mode 100644 tests/unit_tests/test_optimizer_xml_parsing.py diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index c43584d2..9990e5a2 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -66,12 +66,81 @@ def __repr__(self) -> str: feedback=self.feedback, ) +def extract_top_level_blocks(text: str, tag: str): + """Extract all top-level ... blocks from text.""" + blocks = [] + start_tag = f'<{tag}>' + end_tag = f'' + stack = [] + start = None + i = 0 + while i < len(text): + if text.startswith(start_tag, i): + if not stack: + start = i + len(start_tag) + stack.append(i) + i += len(start_tag) + elif text.startswith(end_tag, i): + if stack: + stack.pop() + if not stack and start is not None: + blocks.append(text[start:i]) + start = None + i += len(end_tag) + else: + i += 1 + return blocks + +def extract_first_top_level_block(text: str, tag: str): + blocks = extract_top_level_blocks(text, tag) + return blocks[0] if blocks else None + +def strip_nested_blocks(text: str, tag: str) -> str: + """Remove all nested ... blocks from text, leaving only the top-level text.""" + result = '' + start_tag = f'<{tag}>' + end_tag = f'' + stack = [] + i = 0 + last = 0 + while i < len(text): + if text.startswith(start_tag, i): + if not stack: + result += text[last:i] + stack.append(i) + i += len(start_tag) + elif text.startswith(end_tag, i): + if stack: + stack.pop() + if not stack: + last = i + len(end_tag) + i += len(end_tag) + else: + i += 1 + if not stack: + result += text[last:] + return result.strip() + +def extract_reasoning_and_remainder(text: str): + """Extract reasoning and the remainder of the text after reasoning block (if closed). Strip whitespace only if properly closed.""" + start_tag = '' + end_tag = '' + start = text.find(start_tag) + if start == -1: + return '', text + start += len(start_tag) + end = text.find(end_tag, start) + if end == -1: + # If not properly closed, don't strip whitespace to preserve original formatting + return text[start:], '' + return text[start:end].strip(), text[end+len(end_tag):] + def extract_xml_like_data(text: str) -> Dict[str, Any]: """ Extract thinking content and improved variables from text containing XML-like tags. Args: - text (str): Text containing and tags + text (str): Text containing and tags Returns: Dict containing: @@ -83,44 +152,29 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: 'variables': {} } - # Extract thinking content - think_pattern = r'(.*?)' - think_match = re.search(think_pattern, text, re.DOTALL) - if think_match: - result['reasoning'] = think_match.group(1).strip() - - # Extract improved variables - # Find all improved_variable blocks - var_pattern = r'(.*?)' - var_matches = re.findall(var_pattern, text, re.DOTALL) - - for var_content in var_matches: - # Extract name - name_pattern = r'(.*?)' - name_match = re.search(name_pattern, var_content, re.DOTALL) - - # Extract value - value_pattern = r'(.*?)' - value_match = re.search(value_pattern, var_content, re.DOTALL) - - if name_match and value_match: - var_name = name_match.group(1).strip() - var_value = value_match.group(1).strip() - - if var_name: # Only add if name is not empty + # Extract reasoning and the remainder of the text + reasoning, remainder = extract_reasoning_and_remainder(text) + result['reasoning'] = reasoning + + # Only parse variables from the remainder (i.e., after a closed reasoning tag) + variable_blocks = extract_top_level_blocks(remainder, 'variable') + for var_block in variable_blocks: + name_block = extract_first_top_level_block(var_block, 'name') + value_block = extract_first_top_level_block(var_block, 'value') + # Only add if both name and value tags are present and name is non-empty after stripping + if name_block is not None and value_block is not None: + var_name = strip_nested_blocks(name_block, 'name').strip() + var_value = value_block.strip() if value_block is not None else '' + if var_name: # Only require name to be non-empty, value can be empty result['variables'][var_name] = var_value - return result + # TODO: solution1 -> solution2 -> solution3 # TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 # TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases class OptoPrimeV2(OptoPrime): - # TODO: 1. merge variable and constraint (DONE) - # TODO: 2. Compact representation: some node is very long to describe in text, show a truncated version (long list of data) - # TODO: if the node displaying, if the string description is too long, we should have a limit on character we send to LLM, display truncated format - # TODO: (a flag to set it) # TODO: LLM has the option to check the value of truncated one # TODO: turn into a conversation round # TODO: and show in a separate message diff --git a/tests/unit_tests/test_optimizer_xml_parsing.py b/tests/unit_tests/test_optimizer_xml_parsing.py new file mode 100644 index 00000000..def41033 --- /dev/null +++ b/tests/unit_tests/test_optimizer_xml_parsing.py @@ -0,0 +1,336 @@ +import re +import unittest +from typing import Dict, Any +from opto.optimizers.optoprime_v2 import extract_xml_like_data + +""" +1. Nested Tag Handling: The parser now uses a stack-based approach to extract only top-level tags, ignoring nested ones: +- containing nested tags → only extracts the top-level value +- containing nested tags → only extracts the top-level name text +- Complex multi-level nesting → correctly handles all levels + +2. Edge Case Handling: +- Empty tags: Allows variables with empty values if tag is present +- Missing tags: Only adds variables if both and tags are present +- Malformed XML: Handles unclosed tags gracefully +- Whitespace: Proper handling of leading/trailing whitespace +- Special characters: Handles < > & " ' characters correctly +- Duplicate variable names: Later variables override earlier ones + +3. Comprehensive Test Coverage (13 tests): +- Basic parsing functionality +- Nested variable/name/value tags +- Multiple nested levels +- Empty tags +- Missing tags +- Malformed XML +- Special characters +- Whitespace handling +- Duplicate variable names +- No reasoning/variable tags scenarios +""" + + + +class TestXMLParsing(unittest.TestCase): + + def test_basic_parsing(self): + """Test basic parsing functionality""" + text = """ + + This is my reasoning for the changes. + + + + var1 + value1 + + + + var2 + value2 + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'This is my reasoning for the changes.', + 'variables': { + 'var1': 'value1', + 'var2': 'value2' + } + } + self.assertEqual(result, expected) + + def test_nested_variable_tags(self): + """Test that only top-level variable tags are extracted""" + text = """ + Reasoning here + + + outer_var + + + inner_var + inner_value + + outer_value + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning here', + 'variables': { + 'outer_var': '\n inner_var\n inner_value\n \n outer_value' + } + } + self.assertEqual(result, expected) + + def test_nested_name_tags(self): + """Test that only top-level name tags are extracted""" + text = """ + Reasoning here + + + + inner_name + outer_name + + some_value + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning here', + 'variables': { + 'outer_name': 'some_value' + } + } + self.assertEqual(result, expected) + + def test_nested_value_tags(self): + """Test that only top-level value tags are extracted""" + text = """ + Reasoning here + + + var_name + + inner_value + outer_value + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning here', + 'variables': { + 'var_name': 'inner_value\n outer_value' + } + } + self.assertEqual(result, expected) + + def test_multiple_nested_levels(self): + """Test complex nested structure""" + text = """ + Complex reasoning + + + level1_name + + + level2_name + + + level3_name + level3_value + + level2_value + + + level1_value + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Complex reasoning', + 'variables': { + 'level1_name': '\n level2_name\n \n \n level3_name\n level3_value\n \n level2_value\n \n \n level1_value' + } + } + self.assertEqual(result, expected) + + def test_empty_tags(self): + """Test handling of empty tags""" + text = """ + + + + + some_value + + + + valid_name + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': '', + 'variables': { + 'valid_name': '' + } + } + self.assertEqual(result, expected) + + def test_missing_tags(self): + """Test handling of missing tags""" + text = """ + Some reasoning + + + var1 + + + + value2 + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Some reasoning', + 'variables': {} + } + self.assertEqual(result, expected) + + def test_malformed_xml(self): + """Test handling of malformed XML""" + text = """ + Reasoning + + var1 + value1 + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning\n \n var1\n value1\n \n ', + 'variables': {} + } + self.assertEqual(result, expected) + + def test_no_reasoning_tag(self): + """Test when reasoning tag is missing""" + text = """ + + var1 + value1 + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': '', + 'variables': { + 'var1': 'value1' + } + } + self.assertEqual(result, expected) + + def test_no_variable_tags(self): + """Test when no variable tags are present""" + text = """ + Just reasoning, no variables + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Just reasoning, no variables', + 'variables': {} + } + self.assertEqual(result, expected) + + def test_whitespace_handling(self): + """Test proper whitespace handling""" + text = """ + + Reasoning with + multiple lines + + + + var_name + + value with + multiple lines + + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning with\n multiple lines', + 'variables': { + 'var_name': 'value with\n multiple lines' + } + } + self.assertEqual(result, expected) + + def test_special_characters(self): + """Test handling of special characters""" + text = """ + Reasoning with < > & " ' characters + + + var_with_special_chars + Value with < > & " ' characters + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning with < > & " \' characters', + 'variables': { + 'var_with_special_chars': 'Value with < > & " \' characters' + } + } + self.assertEqual(result, expected) + + def test_duplicate_variable_names(self): + """Test that later variables override earlier ones with same name""" + text = """ + Reasoning + + + duplicate_var + first_value + + + + duplicate_var + second_value + + """ + + result = extract_xml_like_data(text) + expected = { + 'reasoning': 'Reasoning', + 'variables': { + 'duplicate_var': 'second_value' + } + } + self.assertEqual(result, expected) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 101f8a80a4b6844e33a051f89f8a6f8e75f511fd Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 9 Jul 2025 23:52:06 -0400 Subject: [PATCH 093/172] update --- opto/optimizers/optoprime_v2.py | 105 ++++++++++++++++---------------- 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 9990e5a2..f0d77d0b 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -15,57 +15,6 @@ import re from typing import Dict, Any -@dataclass -class ProblemInstance: - instruction: str - code: str - documentation: str - variables: str - inputs: str - others: str - outputs: str - feedback: str - - problem_template = dedent( - """ - # Instruction - {instruction} - - # Code - {code} - - # Documentation - {documentation} - - # Variables - {variables} - - # Inputs - {inputs} - - # Others - {others} - - # Outputs - {outputs} - - # Feedback - {feedback} - """ - ) - - def __repr__(self) -> str: - return self.problem_template.format( - instruction=self.instruction, - code=self.code, - documentation=self.documentation, - variables=self.variables, - inputs=self.inputs, - outputs=self.outputs, - others=self.others, - feedback=self.feedback, - ) - def extract_top_level_blocks(text: str, tag: str): """Extract all top-level ... blocks from text.""" blocks = [] @@ -169,6 +118,60 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: result['variables'][var_name] = var_value return result +@dataclass +class ProblemInstance: + instruction: str + code: str + documentation: str + variables: str + inputs: str + others: str + outputs: str + feedback: str + + problem_template = dedent( + """ + # Instruction + {instruction} + + # Code + {code} + + # Documentation + {documentation} + + # Variables + {variables} + + # Inputs + {inputs} + + # Others + {others} + + # Outputs + {outputs} + + # Feedback + {feedback} + """ + ) + + def __repr__(self) -> str: + return self.problem_template.format( + instruction=self.instruction, + code=self.code, + documentation=self.documentation, + variables=self.variables, + inputs=self.inputs, + outputs=self.outputs, + others=self.others, + feedback=self.feedback, + ) + +class OptimizerPromptTagSet: + """By inheriting this class and pass into the optimizer. People can change the optimizer documentation""" + pass # TODO: solution1 -> solution2 -> solution3 # TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 From 58ef4b3048193dabd6ab7c35c4188cca38a21cb2 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:14:58 -0400 Subject: [PATCH 094/172] commit an intermediate version (not cleaned up) --- opto/optimizers/optoprime_v2.py | 388 ++++++++++++++++++++++---------- 1 file changed, 274 insertions(+), 114 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index f0d77d0b..9408833e 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -1,8 +1,8 @@ import json from typing import Any, List, Dict, Union, Tuple -from textwrap import dedent, indent from dataclasses import dataclass, asdict -from opto.optimizers.optoprime import OptoPrime +from opto.optimizers.optoprime import OptoPrime, FunctionFeedback +from opto.trace.utils import dedent from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator @@ -15,6 +15,7 @@ import re from typing import Dict, Any + def extract_top_level_blocks(text: str, tag: str): """Extract all top-level ... blocks from text.""" blocks = [] @@ -40,10 +41,12 @@ def extract_top_level_blocks(text: str, tag: str): i += 1 return blocks + def extract_first_top_level_block(text: str, tag: str): blocks = extract_top_level_blocks(text, tag) return blocks[0] if blocks else None + def strip_nested_blocks(text: str, tag: str) -> str: """Remove all nested ... blocks from text, leaving only the top-level text.""" result = '' @@ -70,10 +73,11 @@ def strip_nested_blocks(text: str, tag: str) -> str: result += text[last:] return result.strip() -def extract_reasoning_and_remainder(text: str): + +def extract_reasoning_and_remainder(text: str, tag: str = "reasoning"): """Extract reasoning and the remainder of the text after reasoning block (if closed). Strip whitespace only if properly closed.""" - start_tag = '' - end_tag = '' + start_tag = f'<{tag}>' + end_tag = f'' start = text.find(start_tag) if start == -1: return '', text @@ -82,9 +86,13 @@ def extract_reasoning_and_remainder(text: str): if end == -1: # If not properly closed, don't strip whitespace to preserve original formatting return text[start:], '' - return text[start:end].strip(), text[end+len(end_tag):] + return text[start:end].strip(), text[end + len(end_tag):] + -def extract_xml_like_data(text: str) -> Dict[str, Any]: +def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", + improved_variable_tag: str = "improved_variable", + name_tag: str = "name", + value_tag: str = "value") -> Dict[str, Any]: """ Extract thinking content and improved variables from text containing XML-like tags. @@ -102,22 +110,23 @@ def extract_xml_like_data(text: str) -> Dict[str, Any]: } # Extract reasoning and the remainder of the text - reasoning, remainder = extract_reasoning_and_remainder(text) + reasoning, remainder = extract_reasoning_and_remainder(text, reasoning_tag) result['reasoning'] = reasoning # Only parse variables from the remainder (i.e., after a closed reasoning tag) - variable_blocks = extract_top_level_blocks(remainder, 'variable') + variable_blocks = extract_top_level_blocks(remainder, improved_variable_tag) for var_block in variable_blocks: - name_block = extract_first_top_level_block(var_block, 'name') - value_block = extract_first_top_level_block(var_block, 'value') + name_block = extract_first_top_level_block(var_block, name_tag) + value_block = extract_first_top_level_block(var_block, value_tag) # Only add if both name and value tags are present and name is non-empty after stripping if name_block is not None and value_block is not None: - var_name = strip_nested_blocks(name_block, 'name').strip() + var_name = strip_nested_blocks(name_block, name_tag).strip() var_value = value_block.strip() if value_block is not None else '' if var_name: # Only require name to be non-empty, value can be empty result['variables'][var_name] = var_value return result + @dataclass class ProblemInstance: instruction: str @@ -169,9 +178,74 @@ def __repr__(self) -> str: feedback=self.feedback, ) -class OptimizerPromptTagSet: - """By inheriting this class and pass into the optimizer. People can change the optimizer documentation""" - pass +class OptimizerPromptSymbolSet: + """ + By inheriting this class and pass into the optimizer. People can change the optimizer documentation + + This divides into three parts: + - Section titles: the title of each section in the prompt + - Node tags: the tags that capture the graph structure (only tag names are allowed to be changed) + - Output format: the format of the output of the optimizer + """ + + variables_section_title = "# Variables" + inputs_section_title = "# Inputs" + outputs_section_title = "# Outputs" + others_section_title = "# Others" + feedback_section_title = "# Feedback" + instruction_section_title = "# Instruction" + code_section_title = "# Code" + documentation_section_title = "# Documentation" + + node_tag = "node" # nodes that are constants in the graph + variable_tag = "variable" # nodes that can be changed + value_tag = "value" # inside node, we have value tag + constraint_tag = "constraint" # inside node, we have constraint tag + + # output format + # Note: we currently don't support extracting format's like "```code```" because we assume supplied tag is name-only, i.e., + reasoning_tag = "reasoning" + improved_variable_tag = "variable" + name_tag = "name" + value_tag = "value" + + # custom output format (this will give the highest degree of freedom) + # once it's set, it will override the default output format + output_format_prompt_instruction = None + + def output_response_extractor(self, response: str) -> Dict[str, Any]: + if self.output_format_prompt_instruction is None: + extracted_data = extract_xml_like_data(response, + reasoning_tag=self.reasoning_tag, + improved_variable_tag=self.improved_variable_tag, + name_tag=self.name_tag, + value_tag=self.value_tag) + return extracted_data + else: + raise NotImplementedError( + "If you supplied a custom output format prompt template, you need to implement your own response extractor") + +class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): + variables_section_title = "# Variables" + inputs_section_title = "# Inputs" + outputs_section_title = "# Outputs" + others_section_title = "# Others" + feedback_section_title = "# Feedback" + instruction_section_title = "# Instruction" + code_section_title = "# Code" + documentation_section_title = "# Documentation" + + node_tag = "const" # nodes that are constants in the graph + variable_tag = "var" # nodes that can be changed + value_tag = "data" # inside node, we have value tag + constraint_tag = "constraint" # inside node, we have constraint tag + + # output format + reasoning_tag = "reason" + improved_variable_tag = "var" + name_tag = "name" + value_tag = "data" + # TODO: solution1 -> solution2 -> solution3 # TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 @@ -204,63 +278,38 @@ class OptoPrimeV2(OptoPrime): You're tasked to solve a coding/algorithm problem. You will see the instruction, the code, the documentation of each function used in the code, and the feedback about the execution result. Specifically, a problem will be composed of the following parts: - - #Instruction: the instruction which describes the things you need to do or the question you should answer. - - #Code: the code defined in the problem. - - #Documentation: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. - - #Variables: the input variables that you can change. - - #Inputs: the values of other inputs to the code, which are not changeable. - - #Others: the intermediate values created through the code execution. - - #Outputs: the result of the code output. - - #Feedback: the feedback about the code's execution result. + - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. + - {code_section_title}: the code defined in the problem. + - {documentation_section_title}: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. + - {variables_section_title}: the input variables that you can change. + - {inputs_section_title}: the values of other inputs to the code, which are not changeable. + - {others_section_title}: the intermediate values created through the code execution. + - {outputs_section_title}: the result of the code output. + - {feedback_section_title}: the feedback about the code's execution result. - In `#Variables`, `#Inputs`, `#Outputs`, and `#Others`, the format is: + In `{variables_section_title}`, `{inputs_section_title}`, `{outputs_section_title}`, and `{others_section_title}`, the format is: For variables we express as this: - - - value - - - constraint_expression - - + {variable_expression_format} - If `(data_type)` is `code`, it means `{value}` is the source code of a python code, which may include docstring and definitions. + If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. """ ) # Optimization - default_objective = "You need to change the `value` of the variables in #Variables to improve the output in accordance to #Feedback." + default_objective = "You need to change the `{value_tag}` of the variables in {variables_section_title} to improve the output in accordance to {feedback_section_title}." output_format_prompt_template = dedent( """ Output_format: Your output should be in the following XML/HTML format: ``` - - Your reasoning on why you made the decision to suggest a new value. You can also use it to explain why you didn't want to change it. - - - - variable_1_name - - new_value - ... - - - - - variable_2_name - - new_value - ... - - + {output_format} ``` - In , explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. + In <{reasoning_tag}>, explain the problem: 1. what the {instruction_section_title} means 2. what the {feedback_section_title} on {outputs_section_title} means to {variables_section_title} considering how {variables_section_title} are used in {code_section_title} and other values in {documentation_section_title}, {inputs_section_title}, {others_section_title}. 3. Reasoning about the suggested changes in {variables_section_title} (if needed) and the expected result. - If you need to suggest a change in the values of #Variables, write down the suggested values in . Remember you can change only the values in #Variables, not others. When of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + If you need to suggest a change in the values of {variables_section_title}, write down the suggested values in <{improved_variable_tag}>. Remember you can change only the values in {variables_section_title}, not others. When `type` of a variable is `code`, you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. If no changes are needed, just output TERMINATE. """ @@ -311,53 +360,59 @@ class OptoPrimeV2(OptoPrime): """ ) - # TODO: add an option to replace XML tags if needed by user - - default_prompt_symbols = { - "variables": "# Variables", - "inputs": "# Inputs", - "outputs": "# Outputs", - "others": "# Others", - "feedback": "# Feedback", - "instruction": "# Instruction", - "code": "# Code", - "documentation": "# Documentation", - } def __init__( - self, - parameters: List[ParameterNode], - llm: AbstractModel = None, - *args, - propagator: Propagator = None, - objective: Union[None, str] = None, - ignore_extraction_error: bool = True, # ignore the type conversion error when extracting updated values from LLM's suggestion - include_example=False, # TODO # include example problem and response in the prompt - memory_size=0, # Memory size to store the past feedback - max_tokens=4096, - log=True, - prompt_symbols=None, - initial_var_char_limit=100, - **kwargs, + self, + parameters: List[ParameterNode], + llm: AbstractModel = None, + *args, + propagator: Propagator = None, + objective: Union[None, str] = None, + ignore_extraction_error: bool = True, + # ignore the type conversion error when extracting updated values from LLM's suggestion + include_example=False, # TODO # include example problem and response in the prompt + memory_size=0, # Memory size to store the past feedback + max_tokens=4096, + log=True, + initial_var_char_limit=100, + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OptimizerPromptSymbolSet(), + **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() - self.objective = objective or self.default_objective - self.example_problem = ProblemInstance.problem_template.format( - instruction=self.default_objective, - code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", - documentation="add: add x and y \nsubtract: subtract y from x", - variables="""\n\n5\n\n\na: a > 0\n\n""", - # constraints="a: a > 0", - outputs="""\n\n1\n\n""", - others="""\n\n6\n\n""", - inputs="""\n\n1\n\n\n\n\n5\n\n""", - feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", - stepsize=1, - ) + self.objective = objective or self.default_objective.format(value_tag=optimizer_prompt_symbol_set.value_tag, + variables_section_title= optimizer_prompt_symbol_set.variables_section_title, + feedback_section_title= optimizer_prompt_symbol_set.feedback_section_title) + self.initial_var_char_limit = initial_var_char_limit + self.optimizer_prompt_symbol_set = optimizer_prompt_symbol_set + # self.example_problem = ProblemInstance.problem_template.format( + # instruction=self.objective, + # code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", + # documentation="add: add x and y \nsubtract: subtract y from x", + # variables="""\n\n5\n\n\na: a > 0\n\n""", + # outputs="""\n\n1\n\n""", + # others="""\n\n6\n\n""", + # inputs="""\n\n1\n\n\n\n\n5\n\n""", + # feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", + # stepsize=1, + # ) + self.example_problem_summary = FunctionFeedback(graph=[(1, 'y = add(x=a,y=b)'), (2, "z = subtract(x=y, y=c)")], + documentation={'add': 'This is an add operator of x and y.', + 'subtract': "subtract y from x"}, + others={'y': (6, None)}, + roots={'a': (5, "a > 0"), + 'b': (1, None), + 'c': (5, None)}, + output={'z': (1, None)}, + user_feedback='The result of the code is not as expected. The result should be 10, but the code returns 1' + ) + self.example_problem_summary.variables = {'a': (5, "a > 0")} + self.example_problem_summary.inputs = {'b': (1, None), 'c': (5, None)} + + self.example_problem = self.problem_instance(self.example_problem_summary) self.example_response = dedent( - """ + f""" In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. @@ -370,17 +425,72 @@ def __init__( """ ) - self.output_format_prompt = self.output_format_prompt_template - self.initial_var_char_limit = initial_var_char_limit self.include_example = include_example self.max_tokens = max_tokens self.log = [] if log else None self.summary_log = [] if log else None self.memory = FIFOBuffer(memory_size) + + self.default_prompt_symbols = { + "variables": self.optimizer_prompt_symbol_set.variables_section_title, + "inputs": self.optimizer_prompt_symbol_set.inputs_section_title, + "outputs": self.optimizer_prompt_symbol_set.outputs_section_title, + "others": self.optimizer_prompt_symbol_set.others_section_title, + "feedback": self.optimizer_prompt_symbol_set.feedback_section_title, + "instruction": self.optimizer_prompt_symbol_set.instruction_section_title, + "code": self.optimizer_prompt_symbol_set.code_section_title, + "documentation": self.optimizer_prompt_symbol_set.documentation_section_title, + } + self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) - if prompt_symbols is not None: - self.prompt_symbols.update(prompt_symbols) + self.initialize_prompt() + + def initialize_prompt(self): + self.representation_prompt = self.representation_prompt.format( + variable_expression_format=dedent(f""" + <{self.optimizer_prompt_symbol_set.variable_tag} name="variable_name" type="data_type"> + <{self.optimizer_prompt_symbol_set.value_tag}> + value + + <{self.optimizer_prompt_symbol_set.constraint_tag}> + constraint_expression + + + """), + value_tag=self.optimizer_prompt_symbol_set.value_tag, + variables_section_title=self.optimizer_prompt_symbol_set.variables_section_title.replace(" ", ""), + inputs_section_title=self.optimizer_prompt_symbol_set.inputs_section_title.replace(" ", ""), + outputs_section_title=self.optimizer_prompt_symbol_set.outputs_section_title.replace(" ", ""), + feedback_section_title=self.optimizer_prompt_symbol_set.feedback_section_title.replace(" ", ""), + instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), + code_section_title=self.optimizer_prompt_symbol_set.code_section_title.replace(" ", ""), + documentation_section_title=self.optimizer_prompt_symbol_set.documentation_section_title.replace(" ", ""), + others_section_title = self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") + ) + self.output_format_prompt = self.output_format_prompt_template.format( + output_format=dedent(f""" + <{self.optimizer_prompt_symbol_set.reasoning_tag}> + reasoning + + <{self.optimizer_prompt_symbol_set.improved_variable_tag}> + <{self.optimizer_prompt_symbol_set.name_tag}>variable_name + <{self.optimizer_prompt_symbol_set.value_tag}> + value + + + """), + reasoning_tag=self.optimizer_prompt_symbol_set.reasoning_tag, + improved_variable_tag=self.optimizer_prompt_symbol_set.improved_variable_tag, + instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), + feedback_section_title=self.optimizer_prompt_symbol_set.feedback_section_title.replace(" ", ""), + outputs_section_title=self.optimizer_prompt_symbol_set.outputs_section_title.replace(" ", ""), + code_section_title=self.optimizer_prompt_symbol_set.code_section_title.replace(" ", ""), + documentation_section_title=self.optimizer_prompt_symbol_set.documentation_section_title.replace(" ", ""), + variables_section_title=self.optimizer_prompt_symbol_set.variables_section_title.replace(" ", ""), + inputs_section_title=self.optimizer_prompt_symbol_set.inputs_section_title.replace(" ", ""), + others_section_title=self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") + ) @staticmethod def repr_node_value(node_dict): @@ -388,29 +498,35 @@ def repr_node_value(node_dict): for k, v in node_dict.items(): if "__code" not in k: constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " - temp_list.append(f"\n{v[0]}\n{constraint_expr}\n\n") + temp_list.append( + f"\n{v[0]}\n{constraint_expr}\n\n") else: constraint_expr = f"\n{v[1]}\n" - temp_list.append(f"\n\n{v[0]}\n\n{constraint_expr}\n\n") + temp_list.append( + f"\n\n{v[0]}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) - def repr_node_value_compact(self, node_dict, xml_root_tag="node"): + def repr_node_value_compact(self, node_dict, node_tag="node", + value_tag="value", constraint_tag="constraint"): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: node_value = self.truncate_expression(v[0], self.initial_var_char_limit) - if v[1] is not None: - constraint_expr = f"\n{v[1]}\n" - temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n\n{node_value}\n\n{constraint_expr}\n\n") + if v[1] is not None and node_tag == self.optimizer_prompt_symbol_set.variable_tag: + constraint_expr = f"<{constraint_tag}>\n{v[1]}\n" + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n<{value_tag}>\n{node_value}\n\n{constraint_expr}\n\n") else: - temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n\n{node_value}\n\n\n") + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n<{value_tag}>\n{node_value}\n\n\n") else: - constraint_expr = f"\n{v[1]}\n" + constraint_expr = f"<{constraint_tag}>\n{v[1]}\n" # we only truncate the function body signature = v[1].replace("The code should start with:\n", "") func_body = v[0].replace(signature, "") node_value = self.truncate_expression(func_body, self.initial_var_char_limit) - temp_list.append(f"<{xml_root_tag} name=\"{k}\" type=\"code\">\n\n{signature}{node_value}\n\n{constraint_expr}\n\n") + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"code\">\n<{value_tag}>\n{signature}{node_value}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) def truncate_expression(self, value, limit): @@ -483,27 +599,72 @@ def problem_instance(self, summary, mask=None): else "" ), variables=( - self.repr_node_value_compact(summary.variables, xml_root_tag="variable") + self.repr_node_value_compact(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Variables" not in mask else "" ), inputs=( - self.repr_node_value_compact(summary.inputs) if "#Inputs" not in mask else "" + self.repr_node_value_compact(summary.inputs, node_tag=self.optimizer_prompt_symbol_set.node_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Inputs" not in mask else "" ), outputs=( - self.repr_node_value_compact(summary.output) if "#Outputs" not in mask else "" + self.repr_node_value_compact(summary.output, node_tag=self.optimizer_prompt_symbol_set.node_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Outputs" not in mask else "" ), others=( - self.repr_node_value_compact(summary.others) if "#Others" not in mask else "" + self.repr_node_value_compact(summary.others, node_tag=self.optimizer_prompt_symbol_set.node_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Others" not in mask else "" ), feedback=summary.user_feedback if "#Feedback" not in mask else "", ) + def _step( + self, verbose=False, mask=None, *args, **kwargs + ) -> Dict[ParameterNode, Any]: + assert isinstance(self.propagator, GraphPropagator) + summary = self.summarize() + system_prompt, user_prompt = self.construct_prompt(summary, mask=mask) + + system_prompt = self.replace_symbols(system_prompt, self.prompt_symbols) + user_prompt = self.replace_symbols(user_prompt, self.prompt_symbols) + + response = self.call_llm( + system_prompt=system_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=self.max_tokens, + ) + + if "TERMINATE" in response: + return {} + + suggestion = self.extract_llm_suggestion(response) + update_dict = self.construct_update_dict(suggestion) + + if self.log is not None: + self.log.append( + { + "system_prompt": system_prompt, + "user_prompt": user_prompt, + "response": response, + } + ) + self.summary_log.append( + {"problem_instance": self.problem_instance(summary), "summary": summary} + ) + + return update_dict def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" - suggestion = extract_xml_like_data(response) + # suggestion = extract_xml_like_data(response) + suggestion = self.optimizer_prompt_symbol_set.output_response_extractor(response) if len(suggestion) == 0: if not self.ignore_extraction_error: @@ -544,4 +705,3 @@ def call_llm( if verbose: print("LLM response:\n", response) return response - From 35f157889da8b61abdefbfb23a0939434ece7500 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:18:22 -0400 Subject: [PATCH 095/172] finished with flexible tag change --- opto/optimizers/optoprime_v2.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 9408833e..95f14920 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -360,7 +360,6 @@ class OptoPrimeV2(OptoPrime): """ ) - def __init__( self, parameters: List[ParameterNode], @@ -386,17 +385,7 @@ def __init__( feedback_section_title= optimizer_prompt_symbol_set.feedback_section_title) self.initial_var_char_limit = initial_var_char_limit self.optimizer_prompt_symbol_set = optimizer_prompt_symbol_set - # self.example_problem = ProblemInstance.problem_template.format( - # instruction=self.objective, - # code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)", - # documentation="add: add x and y \nsubtract: subtract y from x", - # variables="""\n\n5\n\n\na: a > 0\n\n""", - # outputs="""\n\n1\n\n""", - # others="""\n\n6\n\n""", - # inputs="""\n\n1\n\n\n\n\n5\n\n""", - # feedback="The result of the code is not as expected. The result should be 10, but the code returns 1", - # stepsize=1, - # ) + self.example_problem_summary = FunctionFeedback(graph=[(1, 'y = add(x=a,y=b)'), (2, "z = subtract(x=y, y=c)")], documentation={'add': 'This is an add operator of x and y.', 'subtract': "subtract y from x"}, @@ -413,16 +402,16 @@ def __init__( self.example_problem = self.problem_instance(self.example_problem_summary) self.example_response = dedent( f""" - + <{self.optimizer_prompt_symbol_set.reasoning_tag}> In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. - + - - a - + <{self.optimizer_prompt_symbol_set.improved_variable_tag}> + <{self.optimizer_prompt_symbol_set.name_tag}>a + <{self.optimizer_prompt_symbol_set.value_tag}> 10 - - + + """ ) From c12ab3ef69f7f267fbad63f4c00811e135d98136 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:35:39 -0400 Subject: [PATCH 096/172] add test for optoprime_v2 (it can print prompt out) --- .../llm_optimizers_tests/test_optoprime_v2.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/llm_optimizers_tests/test_optoprime_v2.py diff --git a/tests/llm_optimizers_tests/test_optoprime_v2.py b/tests/llm_optimizers_tests/test_optoprime_v2.py new file mode 100644 index 00000000..af09c8b2 --- /dev/null +++ b/tests/llm_optimizers_tests/test_optoprime_v2.py @@ -0,0 +1,129 @@ +import os +import pytest +from opto.trace import bundle, node, GRAPH +import opto.optimizers +import importlib +import inspect +import json +import pickle +from opto.utils.llm import LLM + +from opto import trace +from opto.trace import node, bundle +from opto.optimizers.optoprime_v2 import OptoPrimeV2, OptimizerPromptSymbolSet2 + +# You can override for temporarly testing a specific optimizer ALL_OPTIMIZERS = [TextGrad] # [OptoPrimeMulti] ALL_OPTIMIZERS = [OptoPrime] + +# Skip tests if no API credentials are available +SKIP_REASON = "No API credentials found" +HAS_CREDENTIALS = os.path.exists("OAI_CONFIG_LIST") or os.environ.get("TRACE_LITELLM_MODEL") or os.environ.get( + "OPENAI_API_KEY") +llm = LLM() + + +@pytest.fixture(autouse=True) +def clear_graph(): + """Reset the graph before each test""" + GRAPH.clear() + yield + GRAPH.clear() + + +@pytest.mark.skipif(not HAS_CREDENTIALS, reason=SKIP_REASON) +def test_response_extraction(): + pass + + +def test_tag_template_change(): + num_1 = node(1, trainable=True) + num_2 = node(2, trainable=True, description="<=5") + result = num_1 + num_2 + optimizer = OptoPrimeV2([num_1, num_2], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True, + optimizer_prompt_symbol_set=OptimizerPromptSymbolSet2()) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + assert """""" in part1, "Expected tag to be present in part1" + assert """""" in part2, "Expected tag to be present in part2" + + print(part1) + print(part2) + + +@bundle() +def transform(num): + """Add number""" + return num + 1 + + +@bundle(trainable=True) +def multiply(num): + return num * 5 + + +def test_function_repr(): + num_1 = node(1, trainable=False) + + result = multiply(transform(num_1)) + optimizer = OptoPrimeV2([multiply.parameter], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + function_repr = """ + +def multiply(num): + return num * 5 + + +The code should start with: +def multiply(num): + +""" + + assert function_repr in part2, "Expected function representation to be present in part2" + +def test_big_data_truncation(): + num_1 = node(1, trainable=True) + + list_1 = node([1, 2, 3, 4, 5, 6, 7, 8, 9, 20] * 10, trainable=True) + + result = num_1 + list_1[30] + + optimizer = OptoPrimeV2([num_1, list_1], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True, initial_var_char_limit=10) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + truncated_repr = """ + +[1, 2, 3, ...(skipped due to length limit) + +""" + + assert truncated_repr in part2, "Expected truncated list representation to be present in part2" \ No newline at end of file From 3823d0ee1eda19171c10c005c5b84afe39b3f8b7 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:43:46 -0400 Subject: [PATCH 097/172] fix pyproject file --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd171f07..8d652ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ authors = [ {name = "Adith Swaminathan", email = "adith387@gmail.com"}, ] license="MIT" -license-files=["LICEN[CS]E*"] requires-python = ">= 3.9" dynamic = ["version", "dependencies", "description"] readme = "README.md" @@ -30,3 +29,6 @@ autogen = ["autogen-agentchat==0.2.40"] Homepage = "https://microsoft.github.io/Trace/" Documentation = "https://microsoft.github.io/Trace/intro.html" Repository = "https://github.com/microsoft/Trace.git" + +[tool.setuptools] +license-files = ["LICEN[CS]E*"] \ No newline at end of file From 3167a08f0e995587410ebc5a4a61b71e2063353b Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 12:52:33 -0400 Subject: [PATCH 098/172] quick fix on xml parsing testing --- opto/optimizers/optoprime_v2.py | 2 +- tests/unit_tests/test_optimizer_xml_parsing.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 95f14920..55da6d96 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -90,7 +90,7 @@ def extract_reasoning_and_remainder(text: str, tag: str = "reasoning"): def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", - improved_variable_tag: str = "improved_variable", + improved_variable_tag: str = "variable", name_tag: str = "name", value_tag: str = "value") -> Dict[str, Any]: """ diff --git a/tests/unit_tests/test_optimizer_xml_parsing.py b/tests/unit_tests/test_optimizer_xml_parsing.py index def41033..29df89a7 100644 --- a/tests/unit_tests/test_optimizer_xml_parsing.py +++ b/tests/unit_tests/test_optimizer_xml_parsing.py @@ -30,8 +30,6 @@ - No reasoning/variable tags scenarios """ - - class TestXMLParsing(unittest.TestCase): def test_basic_parsing(self): From 27e8c64342625ddd714ae7d034c5938e5609f620 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 13:42:12 -0400 Subject: [PATCH 099/172] add more cleanup --- opto/optimizers/optoprime_v2.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 55da6d96..ee77d1b3 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -178,6 +178,7 @@ def __repr__(self) -> str: feedback=self.feedback, ) + class OptimizerPromptSymbolSet: """ By inheriting this class and pass into the optimizer. People can change the optimizer documentation @@ -225,6 +226,7 @@ def output_response_extractor(self, response: str) -> Dict[str, Any]: raise NotImplementedError( "If you supplied a custom output format prompt template, you need to implement your own response extractor") + class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): variables_section_title = "# Variables" inputs_section_title = "# Inputs" @@ -261,16 +263,6 @@ class OptoPrimeV2(OptoPrime): # TODO: idea 1: for each operator, we can identify repeated structure # TODO: idea 2: for each bundle/op, the user can pass in a callable function, take original output, return a string # TODO: idea 2-2: each node has a string representation of data, that's what the optimizer should use (this string is fixed) - # TODO: some are too redundant to describe - # TODO: x = a + b - # TODO: y = a + c - # TODO: z = f(x, y) => z = f(a+b, a+c) - # TODO: z = g(a, b, c) - - # TODO: Node level change: format_data_repr(func: Callable[[Node], str]) -> None - # TODO: Check format data representation - # TODO: input would be the data of this node, return would be a string - # TODO: later on optimizer just calls this # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( @@ -369,7 +361,7 @@ def __init__( objective: Union[None, str] = None, ignore_extraction_error: bool = True, # ignore the type conversion error when extracting updated values from LLM's suggestion - include_example=False, # TODO # include example problem and response in the prompt + include_example=False, memory_size=0, # Memory size to store the past feedback max_tokens=4096, log=True, @@ -381,8 +373,8 @@ def __init__( self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() self.objective = objective or self.default_objective.format(value_tag=optimizer_prompt_symbol_set.value_tag, - variables_section_title= optimizer_prompt_symbol_set.variables_section_title, - feedback_section_title= optimizer_prompt_symbol_set.feedback_section_title) + variables_section_title=optimizer_prompt_symbol_set.variables_section_title, + feedback_section_title=optimizer_prompt_symbol_set.feedback_section_title) self.initial_var_char_limit = initial_var_char_limit self.optimizer_prompt_symbol_set = optimizer_prompt_symbol_set @@ -455,7 +447,7 @@ def initialize_prompt(self): instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), code_section_title=self.optimizer_prompt_symbol_set.code_section_title.replace(" ", ""), documentation_section_title=self.optimizer_prompt_symbol_set.documentation_section_title.replace(" ", ""), - others_section_title = self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") + others_section_title=self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") ) self.output_format_prompt = self.output_format_prompt_template.format( output_format=dedent(f""" From 397e98379ea456c4ff82b639d4640ada755fb0f3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 10 Jul 2025 18:35:04 +0000 Subject: [PATCH 100/172] Update naming convention of deepcopied node. --- opto/trace/nodes.py | 18 +++++++++++------- tests/unit_tests/test_nodes.py | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/opto/trace/nodes.py b/opto/trace/nodes.py index 756328e6..cac02446 100644 --- a/opto/trace/nodes.py +++ b/opto/trace/nodes.py @@ -23,11 +23,11 @@ def node(data, name=None, trainable=False, description=None): Notes: If trainable=True: - If data is already a Node, extracts underlying data and updates name - - Creates ParameterNode with extracted data, name, trainable=True + - Creates ParameterNode with extracted data, name, trainable=True If trainable=False: - If data is already a Node, returns it (with warning if name provided) - - Otherwise creates new Node with data, name + - Otherwise creates new Node with data, name """ assert type(description) is str or description is None @@ -456,6 +456,10 @@ def __deepcopy__(self, memo): setattr(result, k, []) elif k == "_feedback": setattr(result, k, defaultdict(list)) + elif k == "_name": + name, counter = v.split(":") + new_name = v.replace(':', '') + '_copy:0' # this allows to keep track with the original name + setattr(result, k, new_name) else: setattr(result, k, copy.deepcopy(v, memo)) GRAPH.register(result) @@ -791,7 +795,7 @@ def __init__( trainable: bool = False, description: str = None, info: Union[None, Dict] = None, - ) -> None: + ) -> None: if description == "" or description is None: description = f"[{type(self).__name__}]" @@ -828,13 +832,13 @@ def feedback(self): @property def description(self): - """A textual description of the node.""" + """A textual description of the node.""" # return self._description # remove the operator type from the description description = re.sub(r"^\[([^\[\]]+)\]", "", self._description).strip() # return None if empty return description if description else None - + @property def op_name(self): """The operator type of the node, extracted from the description.""" @@ -2012,7 +2016,7 @@ def __init__( info=info, ) self._dependencies["parameter"].add(self) - + if projections is not None: assert isinstance( projections, list @@ -2020,7 +2024,7 @@ def __init__( from opto.trace.projections import Projection assert all( isinstance(p, Projection) for p in projections - ), "All projections must be instances of Projection." + ), "All projections must be instances of Projection." self.projections = projections else: self.projections = [] diff --git a/tests/unit_tests/test_nodes.py b/tests/unit_tests/test_nodes.py index 6d5d1e73..31aec23e 100644 --- a/tests/unit_tests/test_nodes.py +++ b/tests/unit_tests/test_nodes.py @@ -87,6 +87,7 @@ def test_node_copy_clone_deepcopy(): z_new = ops.identity(z) z_clone = z.clone() z_copy = copy.deepcopy(z) + assert z_copy.name == z.py_name + '_copy:0' assert z_new.data == z.data assert z_clone.data == z.data assert z_copy.data == z.data From 7177f10040bd75abbeba9f986c9c5dec65e5c508 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 15:11:35 -0400 Subject: [PATCH 101/172] added new test and it passes --- .../unit_tests/test_optimizer_xml_parsing.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/unit_tests/test_optimizer_xml_parsing.py b/tests/unit_tests/test_optimizer_xml_parsing.py index 29df89a7..edbb3758 100644 --- a/tests/unit_tests/test_optimizer_xml_parsing.py +++ b/tests/unit_tests/test_optimizer_xml_parsing.py @@ -329,6 +329,40 @@ def test_duplicate_variable_names(self): } self.assertEqual(result, expected) + def test_xml_with_random_text(self): + """Test that parser extracts XML content while ignoring random text""" + text = """ + This is some random texts with random symbols `~!@#$%^&*()-=[]\;',./_+{}|:"<>?. + + + Some reasoning. + + + Some other random texts with random symbols `~!@#$%^&*()-=[]\;',./_+{}|:"<>?. + + + var1 + value1 + + + Yet another random texts with random symbols `~!@#$%^&*()-=[]\;',./_+{}|:"<>?. + + + var2 + value2 + + """ + + result = extract_xml_like_data(text, name_tag="name", value_tag="value") + expected = { + 'reasoning': 'Some reasoning.', + 'variables': { + 'var1': 'value1', + 'var2': 'value2' + } + } + self.assertEqual(result, expected) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 569c71facc23eebd229d46be701e1c487d99cde9 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 15:16:37 -0400 Subject: [PATCH 102/172] incorporate some of Xavier's suggestion on OptoPrime instruction change --- opto/optimizers/optoprime_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index ee77d1b3..bf0a3de7 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -273,8 +273,8 @@ class OptoPrimeV2(OptoPrime): - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. - {code_section_title}: the code defined in the problem. - {documentation_section_title}: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. - - {variables_section_title}: the input variables that you can change. - - {inputs_section_title}: the values of other inputs to the code, which are not changeable. + - {variables_section_title}: the input variables that you can change/tweak (trainable). + - {inputs_section_title}: the values of fixed inputs to the code, which CANNOT be changed (fixed). - {others_section_title}: the intermediate values created through the code execution. - {outputs_section_title}: the result of the code output. - {feedback_section_title}: the feedback about the code's execution result. From e3d08449baeaed9a5db5fb90a4769d97e2993d4d Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 10 Jul 2025 20:15:46 +0000 Subject: [PATCH 103/172] Add randomize flag and n_epochs attribute to Dataloader. --- opto/trainer/loader.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/opto/trainer/loader.py b/opto/trainer/loader.py index 90d738f9..1da24dca 100644 --- a/opto/trainer/loader.py +++ b/opto/trainer/loader.py @@ -5,12 +5,15 @@ class DataLoader: - def __init__(self, dataset, batch_size=1, replacement=False, shuffle=True): + def __init__(self, dataset, batch_size=1, randomize=True, replacement=False, shuffle=True): """ Initialize the data loader Args: dataset: the dataset to load (a dict of inputs and infos) batch_size: the number of samples to load in each batch + randomize: whether to randomize the dataset ordering before loading; + if False, the dataset will be loaded in the order it is + provided (replacement and shuffle be ignored) replacement: whether to sample with replacement shuffle: whether to shuffle the dataset after each epoch """ @@ -20,9 +23,11 @@ def __init__(self, dataset, batch_size=1, replacement=False, shuffle=True): self.dataset = dataset self.batch_size = batch_size + self.randomize = randomize self.replacement = replacement self.shuffle = shuffle self._indices = self._update_indices() + self.n_epochs = 0 self._i = 0 def __iter__(self): @@ -33,7 +38,9 @@ def __next__(self): if self._i >= len(self._indices): if self.shuffle: self._indices = self._update_indices() + # Reset the index for the next epoch self._i = 0 + self.n_epochs += 1 raise StopIteration indices = self._indices[self._i: min(self._i + self.batch_size, len(self._indices))] xs = [self.dataset['inputs'][ind] for ind in indices] @@ -43,7 +50,10 @@ def __next__(self): def _update_indices(self): N = len(self.dataset['inputs']) - return np.random.choice(N, size=N, replace=self.replacement) + if self.randomize: + return np.random.choice(N, size=N, replace=self.replacement) + else: + return np.arange(N) def sample(self): """ Sample a batch of data from the dataset """ From 151fef5630247f79262bc59486e2c5c48c0357cc Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 18:42:22 -0400 Subject: [PATCH 104/172] remove UCBsearch --- opto/trainer/algorithms/UCBsearch.py | 1513 -------------------------- 1 file changed, 1513 deletions(-) delete mode 100644 opto/trainer/algorithms/UCBsearch.py diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py deleted file mode 100644 index 0f3f9bc3..00000000 --- a/opto/trainer/algorithms/UCBsearch.py +++ /dev/null @@ -1,1513 +0,0 @@ -import numpy as np -import copy -import math -from collections import deque -from typing import Union, List, Tuple, Dict, Any, Optional -from opto import trace -from opto.trainer.utils import async_run # Assuming print_color is in utils -from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful -import json # For LLM output parsing -import random # Added for alpha probability -from opto.utils.llm import LLM # For the selector LLM -from opto.trace.nodes import ParameterNode -import warnings -from black import format_str, FileMode - -class UCBSearchAlgorithm(MinibatchAlgorithm): - """ - UCB Search Algorithm. - - Keeps a buffer of candidates with their statistics (score sum, evaluation count). - In each iteration: - 1. Picks a candidate 'a' from the buffer with the highest UCB score. - 2. Updates the optimizer with 'a's parameters. - 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a''. - 4. Evaluates 'a'' on a validation set minibatch. - 5. Updates statistics of 'a' (based on the training minibatch). - 6. Adds 'a'' (with its validation stats) to the buffer. - 7. If the buffer is full, evicts the candidate with the lowest UCB score. - """ - - def __init__(self, - agent: trace.Module, - optimizer, - max_buffer_size: int = 10, - ucb_exploration_factor: float = 1.0, # Controls exploration vs exploitation tradeoff in UCB selection - # UCB formula: μ(a) + c * sqrt(ln(t) / n(a)), c is the exploration factor - logger=None, - num_threads: int = None, - use_validation: bool = False, - *args, - **kwargs): - super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) - - self.buffer = deque(maxlen=max_buffer_size) - self.max_buffer_size = max_buffer_size - # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, - # lower values favor exploitation of well-performing candidates. - self.ucb_exploration_factor = ucb_exploration_factor - self.use_validation = use_validation # Whether to use validation set for evaluation - # To ensure optimizer_step can be called with bypassing=True if needed. - # This depends on the specific optimizer's implementation. - # For now, we assume the optimizer has a step method that can return parameters. - if not hasattr(self.optimizer, 'step'): - raise ValueError("Optimizer must have a 'step' method.") - - self._total_evaluations_tracker = 0 # Tracks total number of individual candidate evaluations used in UCB calculation for log(T) - self._candidate_id_counter = 0 - - def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: - """Sample a minibatch from the dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos'): - print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') - return [], [] - - dataset_size = len(dataset['inputs']) - if dataset_size == 0: - print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') - return [], [] - - actual_batch_size = min(batch_size, dataset_size) - indices = np.random.choice(dataset_size, actual_batch_size, replace=False) - xs = [dataset['inputs'][i] for i in indices] - infos = [dataset['infos'][i] for i in indices] - return xs, infos - - def _evaluate_candidate(self, - params_to_eval_dict: Dict[str, Any], - dataset: Dict[str, List[Any]], # Changed from validate_dataset - guide, # Changed from validate_guide - evaluation_batch_size: int, # New parameter name - num_threads: Optional[int] = None - ) -> Tuple[float, int]: - """Evaluates a given set of parameters on samples from the provided dataset (now typically train_dataset).""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: - print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') - return -np.inf, 0 - - original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - self.optimizer.update(params_to_eval_dict) - - eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) - - if not eval_xs: - print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') - self.optimizer.update(original_params) - return -np.inf, 0 - - eval_scores = evaluate(self.agent, - guide, # Use main guide - eval_xs, - eval_infos, - min_score=self.min_score if hasattr(self, 'min_score') else None, - num_threads=num_threads or self.num_threads, - description=f"Evaluating candidate") - - self.optimizer.update(original_params) - - avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else 0 - eval_count = len(eval_xs) - - return float(avg_score), eval_count - - def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates UCB score for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('inf') # Explore unvisited states first - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall - # and to ensure log argument is > 0. - # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. - if total_tracked_evaluations == 0: # Should not happen if we init with one eval - total_tracked_evaluations = 1 - - # UCB exploration term: ucb_exploration_factor scales the confidence interval - # Higher factor = more exploration, lower factor = more exploitation - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) - - return mean_score + exploration_term - - def _calculate_lcb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates Lower Confidence Bound for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('-inf') # Unvisited states get lowest bound - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall - # and to ensure log argument is > 0. - # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. - if total_tracked_evaluations == 0: # Should not happen if we init with one eval - total_tracked_evaluations = 1 - - # LCB exploration term: ucb_exploration_factor scales the confidence interval - # Higher factor = more exploration, lower factor = more exploitation - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) - - return mean_score - exploration_term - - def _update_buffer_ucb_scores(self): - """Recalculates and updates UCB scores for all candidates in the buffer.""" - if not self.buffer: - return - - for candidate_entry in self.buffer: - candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) - - def _get_best_candidate_from_buffer(self, buffer): - """Get the best candidate from buffer, excluding those with eval_count = 0 when not using validation.""" - if not buffer: - return None - - # Filter out candidates with eval_count = 0 if not using validation - if not self.use_validation: - valid_candidates = [c for c in buffer if c['eval_count'] > 0] - if not valid_candidates: - # If no candidates have been evaluated, return the one with highest UCB score - return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) - return max(valid_candidates, key=lambda c: c['score_sum'] / c['eval_count']) - else: - # When using validation, all candidates should have eval_count > 0 - return max(buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) - - def print_intervals(self, buffer): - """Print confidence intervals for debugging in the form of open intervals (LCB, UCB)""" - print_color("Confidence intervals for all candidates:", 'cyan') - for i, candidate_entry in enumerate(buffer): - lcb = self._calculate_lcb(candidate_entry, self._total_evaluations_tracker) - ucb = candidate_entry['ucb_score'] - mean_score = candidate_entry['score_sum'] / (candidate_entry['eval_count'] or 1) - eval_count = candidate_entry['eval_count'] - - # Format as open interval (LCB, UCB) with mean score and evaluation count - interval_str = f"Action {i+1}: ({lcb:.4f}, {ucb:.4f}) [mean: {mean_score:.4f}, n: {eval_count}]" - print_color(interval_str, 'cyan') - - def _process_single_candidate(self, - action_candidate_a: Dict, - guide, - train_dataset: Dict[str, List[Any]], - validation_dataset: Dict[str, List[Any]], - train_batch_size: int, - evaluation_batch_size: int, - num_threads: Optional[int], - iteration: int) -> Tuple[bool, float, float, int]: - """ - Process a single candidate: generate a_prime, evaluate both a and a_prime, - update stats for 'a', and add 'a_prime' to buffer. - - Returns: - Tuple of (success, a_prime_score, score_for_a_on_train_batch, samples_used) - """ - # 2. Load parameters of 'a' into the agent for the optimizer update step - self.optimizer.update(action_candidate_a['params']) - - # 3. Draw minibatch from the training set, do update from 'a' to get 'a_prime' - train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) - if not train_xs: - print_color(f"Iter {iteration}: Training minibatch empty for candidate, skipping.", 'yellow') - return False, -np.inf, -np.inf, 0 - - # Perform forward pass and get feedback for agent parameters 'a' - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - outputs_for_a = async_run([self.forward]*len(train_xs), - [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], - max_workers=num_threads, - description=f"Iter {iteration}: Forward pass for action 'a'") - else: - outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] - - scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] - for target, score, feedback in outputs_for_a: - scores_from_train.append(score) - targets_from_train.append(target) - feedbacks_from_train.append(feedback) - - if not scores_from_train: - print_color(f"Iter {iteration}: No outputs from forward pass for candidate. Skipping.", 'yellow') - return False, -np.inf, -np.inf, 0 - - target_for_a = batchify(*targets_from_train) - feedback_for_a = batchify(*feedbacks_from_train).data - score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf - - self.optimizer.zero_feedback() - self.optimizer.backward(target_for_a, feedback_for_a) - - try: - a_prime_params_dict = self.optimizer.step(bypassing=True, verbose=False) - if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: - print_color(f"Iter {iteration}: Optimizer.step did not return valid params. Using current agent params.", 'yellow') - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - self.total_proposals += 1 - except Exception as e: - print_color(f"Iter {iteration}: Error during optimizer.step: {e}. Skipping.", 'red') - return False, -np.inf, -np.inf, 0 - - # 4. Evaluate 'a' and 'a_prime' on samples of validation set in parallel - if self.use_validation: - if use_asyncio: - evaluation_results = async_run( - [self._evaluate_candidate, self._evaluate_candidate], - [ - (action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads), - (a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads) - ], - max_workers=2, - description=f"Iter {iteration}: Parallel evaluation of 'a' and 'a_prime'" - ) - (a_score, a_evals), (a_prime_score, a_prime_evals) = evaluation_results - else: - a_score, a_evals = self._evaluate_candidate( - action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads - ) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - - # 5. Update statistics for the original candidate 'a' - # Always update statistics for the original candidate 'a' on the training set - if score_for_a_on_train_batch > -np.inf: - action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) - action_candidate_a['eval_count'] += len(train_xs) - self._total_evaluations_tracker += len(train_xs) - - # If we use validation set for evaluation - if self.use_validation: # If we use validation set for evaluation - action_candidate_a['score_sum'] += a_score * a_evals - action_candidate_a['eval_count'] += a_evals - - # 6. Add 'a_prime' to the buffer (with eviction logic if needed) - if a_prime_score > -np.inf and a_prime_evals > 0: - new_candidate_entry = { - 'params': a_prime_params_dict, - 'score_sum': a_prime_score * a_prime_evals, - 'eval_count': a_prime_evals, - 'ucb_score': None, # Will be updated later - 'iteration_created': iteration - } - - # Eviction logic before adding if buffer is at max capacity - if len(self.buffer) >= self.max_buffer_size: - self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction - candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) - self.buffer.remove(candidate_to_evict) - print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') - - self.buffer.append(new_candidate_entry) - print_color(f"Iter {iteration}: Added new candidate to buffer (score: {a_prime_score:.4f})", 'magenta') - else: - print_color(f"Iter {iteration}: New candidate a_prime had invalid score/evals, not added to buffer.", 'yellow') - - # Update tracking - self._total_evaluations_tracker += a_evals + a_prime_evals - samples_used = 2 * evaluation_batch_size + train_batch_size - else: # If we don't use validation set for evaluation, please evaluate a_prime on the training set - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, {'inputs': train_xs, 'infos': train_infos}, - guide, len(train_xs), num_threads - ) - self._total_evaluations_tracker += a_prime_evals - - new_candidate_entry = { - 'params': a_prime_params_dict, - 'score_sum': a_prime_score * a_prime_evals if a_prime_score > -np.inf else 0, - 'eval_count': a_prime_evals, - 'ucb_score': None, # Will be updated later - 'iteration_created': iteration - } - self.buffer.append(new_candidate_entry) - samples_used = 2*train_batch_size # One batch for training update, one for evaluation - return True, a_prime_score, score_for_a_on_train_batch, samples_used - - def train(self, - guide, # Guide for train_dataset (feedback generation AND evaluation) - train_dataset: Dict[str, List[Any]], - *, - validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset - test_dataset: Optional[Dict[str, List[Any]]] = None, - num_search_iterations: int = 100, - train_batch_size: int = 2, - evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations - eval_frequency: int = 1, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/ucb_agent.pkl", - min_score_for_agent_update: Optional[float] = None, # Renamed from min_score to avoid conflict with evaluate's min_score - verbose: Union[bool, str] = False, - num_threads: Optional[int] = None, - print_confidence_interval: bool = True, - **kwargs - ) -> Tuple[Dict[str, Any], float]: # Returns metrics and best score - """ - Main training loop for UCB Search Algorithm. - """ - # Default validation_dataset to train_dataset if not provided - if validation_dataset is None: - validation_dataset = train_dataset - if test_dataset is None: - test_dataset = train_dataset - - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency - self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate - total_samples = 0 - self.total_proposals = 0 - # Metrics tracking - metrics = { - 'best_candidate_scores': [], # Score of the best candidate (e.g., highest mean) found so far at each iteration - 'selected_action_ucb': [], # UCB score of the selected action 'a' - 'new_candidate_scores': [], # Score of the new candidate 'a_prime' - 'buffer_avg_score': [], - 'buffer_avg_evals': [], - } - -# 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') - initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide - ) - self.logger.log('Test score', initial_score, 0, color='blue') - self.logger.log('Total samples', total_samples, 0, color='cyan') - print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') - if self.use_validation: - self._total_evaluations_tracker += initial_evals - total_samples += initial_evals - # Log initial evaluation - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later - 'eval_count': initial_evals, - 'ucb_score': None, # avoid accidental reads before it's initialized - 'iteration_created': 0 - } - self._update_buffer_ucb_scores() # Update UCB for the initial candidate - else: - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': 0, - 'eval_count': 0, - 'ucb_score': None, # avoid accidental reads before it's initialized - 'iteration_created': 0 - } - self.buffer.append(initial_candidate_entry) - - # Main search loop - for iteration in range(1, num_search_iterations + 1): - try: - if not self.buffer: - print_color("Buffer is empty, stopping search.", 'red') - break - - # 1. Pick the candidate 'a' with the highest UCB from the buffer - self._update_buffer_ucb_scores() # Ensure UCB scores are fresh - - action_candidate_a = self.select(self.buffer) - if print_confidence_interval: - self.print_intervals(self.buffer) - # Log selected action UCB score - self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') - self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') - - print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') - - # Process the selected candidate - success, a_prime_score, score_for_a_on_train_batch, samples_used = self._process_single_candidate( - action_candidate_a, guide, train_dataset, validation_dataset, - train_batch_size, evaluation_batch_size, num_threads, iteration - ) - - if not success: # Error occurred in processing - continue - - total_samples += samples_used - if self.use_validation: - metrics['new_candidate_scores'].append(a_prime_score) - self.logger.log('New candidate score', a_prime_score, iteration, color='green') - print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}", 'cyan') - self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') - - - - # Update all UCB scores in the buffer after potential additions/removals/stat updates - self._update_buffer_ucb_scores() - - # Logging - best_in_buffer = self._get_best_candidate_from_buffer(self.buffer) - if best_in_buffer: - metrics['best_candidate_scores'].append(best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1)) - else: - metrics['best_candidate_scores'].append(-np.inf) - metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) - metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) - - if iteration % log_frequency == 0: - log_data = { - "iteration": iteration, - "best_score": metrics['best_candidate_scores'][-1], #best_candidate_score_in_buffer - "selected_action_ucb": action_candidate_a['ucb_score'], - "new_candidate_score": a_prime_score, - "buffer_size": len(self.buffer), - "buffer_avg_score": metrics['buffer_avg_score'][-1], - "buffer_avg_evals": metrics['buffer_avg_evals'][-1], - "total_evaluations_tracker": self._total_evaluations_tracker, # used in calculating ucb scores - "total_samples": total_samples # Add new metric - } - - # Log all important metrics - self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') - self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') - self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') - self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') - # self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') - self.logger.log('Total samples', log_data['total_samples'], iteration, color='yellow') - self.logger.log('Total proposals', self.total_proposals, iteration, color='red') - print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') - - if test_dataset is not None and iteration % eval_frequency == 0: - try: - # Save current agent parameters - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - - # Find the best candidate in the buffer (highest mean score) - best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_candidate: - print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') - continue - - # Load best candidate's parameters into the agent for evaluation - self.optimizer.update(best_candidate['params']) - - # Evaluate the best candidate on test set - test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=self.min_score, num_threads=num_threads, - description=f"Evaluating best candidate (iteration {iteration})") - - # Restore original agent parameters - self.optimizer.update(current_params) - - self.logger.log('Test score', test_score, iteration, color='green') - except Exception as e: - print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') - - # Save agent (e.g., the one with highest mean score in buffer) - if save_frequency is not None and iteration % save_frequency == 0: - try: - best_overall_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_overall_candidate: - print_color(f"Iter {iteration}: No valid candidate for agent save.", 'yellow') - continue - self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer - self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase - print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') - except Exception as e: - print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') - - except Exception as e: - print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') - self.logger.log('Iteration error', str(e), iteration, color='red') - continue - - # End of search loop - print_color("UCB search finished.", 'blue') - - # Log final training summary - final_iteration = num_search_iterations - self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') - self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') - - if not self.buffer: - print_color("Buffer is empty at the end of search. No best candidate found.", 'red') - self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') - return metrics, -np.inf - - # Select the best candidate based on highest mean score (exploitation) - final_best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not final_best_candidate: - print_color("No valid candidate found at the end of search.", 'red') - return metrics, -np.inf - final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) - - # Log final results - self.logger.log('Final best score', final_best_score, final_iteration, color='green') - self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') - self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') - - print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') - - # Load best parameters into the agent - self.optimizer.update(final_best_candidate['params']) # Load params using optimizer - - return metrics, float(final_best_score) - - def select(self, buffer): - '''Could be subclassed to implement different selection strategies''' - return max(buffer, key=lambda c: c['ucb_score']) - - -class UCBSearchParallelAlgorithm(UCBSearchAlgorithm): - """ - Parallel UCB Search Algorithm. - - Instead of selecting one candidate with highest UCB score, selects top-k candidates - and processes them in parallel to generate k new candidates per iteration. - """ - - def __init__(self, - agent: trace.Module, - optimizer, - max_buffer_size: int = 10, - ucb_exploration_factor: float = 1.0, - parallel_k: int = 2, # Number of top candidates to process in parallel - logger=None, - num_threads: int = None, - *args, - **kwargs): - super().__init__(agent, optimizer, max_buffer_size, ucb_exploration_factor, - logger, num_threads, *args, **kwargs) - self.parallel_k = parallel_k - - def select_top_k(self, buffer, k): - """Select top k candidates with highest UCB scores""" - if len(buffer) <= k: - return buffer.copy() - - # Sort by UCB score and return top k - sorted_candidates = sorted(buffer, key=lambda c: c['ucb_score'], reverse=True) - return sorted_candidates[:k] - - def train(self, - guide, - train_dataset: Dict[str, List[Any]], - *, - validation_dataset: Optional[Dict[str, List[Any]]] = None, - test_dataset: Optional[Dict[str, List[Any]]] = None, - num_search_iterations: int = 100, - train_batch_size: int = 2, - evaluation_batch_size: int = 20, - eval_frequency: int = 1, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/ucb_parallel_agent.pkl", - min_score_for_agent_update: Optional[float] = None, - verbose: Union[bool, str] = False, - num_threads: Optional[int] = None, - print_confidence_interval: bool = True, - **kwargs - ) -> Tuple[Dict[str, Any], float]: - """ - Main training loop for Parallel UCB Search Algorithm. - """ - # Default validation_dataset to train_dataset if not provided - if validation_dataset is None: - validation_dataset = train_dataset - if test_dataset is None: - test_dataset = train_dataset - - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency - self.min_score = min_score_for_agent_update - total_samples = 0 - self.total_proposals = 0 - - # Metrics tracking - metrics = { - 'best_candidate_scores': [], - 'selected_actions_ucb': [], # UCB scores of selected top-k actions - 'new_candidate_scores': [], # Scores of all new candidates - 'buffer_avg_score': [], - 'buffer_avg_evals': [], - 'parallel_k_used': [], # Track how many candidates were actually processed - } - - # Initialize with first candidate (same as parent) - print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += initial_evals - total_samples += initial_evals - - # Log initial evaluation - self.logger.log('Initial UCB score', initial_score, 0, color='blue') - self.logger.log('Total samples', total_samples, 0, color='cyan') - - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, - 'eval_count': initial_evals, - 'ucb_score': None, - 'iteration_created': 0 - } - self.buffer.append(initial_candidate_entry) - self._update_buffer_ucb_scores() - print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') - - # Main search loop - for iteration in range(1, num_search_iterations + 1): - try: - if not self.buffer: - print_color("Buffer is empty, stopping search.", 'red') - break - - # 1. Select top-k candidates with highest UCB scores - self._update_buffer_ucb_scores() - top_k_candidates = self.select_top_k(self.buffer, self.parallel_k) - - if print_confidence_interval: - self.print_intervals(self.buffer) - - print_color(f"Iter {iteration}/{num_search_iterations}: Processing {len(top_k_candidates)} candidates in parallel", 'blue') - - # Log selected actions UCB scores - selected_ucb_scores = [c['ucb_score'] for c in top_k_candidates] - metrics['selected_actions_ucb'].append(selected_ucb_scores) - avg_selected_ucb = np.mean(selected_ucb_scores) - self.logger.log('Average selected UCB', avg_selected_ucb, iteration, color='magenta') - - # 2. Process all top-k candidates sequentially - candidate_results = [] - for candidate in top_k_candidates: - result = self._process_single_candidate( - candidate, guide, train_dataset, validation_dataset, - train_batch_size, evaluation_batch_size, num_threads, iteration - ) - candidate_results.append(result) - - # 3. Process results and update statistics - iteration_new_scores = [] - - for i, (candidate, result) in enumerate(zip(top_k_candidates, candidate_results)): - success, a_prime_score, score_for_a_on_train_batch, samples_used = result - - if not success: # Error occurred - print_color(f"Iter {iteration}: Candidate {i+1} processing failed, skipping.", 'yellow') - continue - # Track new candidate score - iteration_new_scores.append(a_prime_score) - - # Update tracking - total_samples += samples_used - - metrics['new_candidate_scores'].extend(iteration_new_scores) - - # Log iteration performance - if iteration_new_scores: - avg_new_score = np.mean(iteration_new_scores) - max_new_score = max(iteration_new_scores) - self.logger.log('New candidate score', avg_new_score, iteration, color='green') #average new candidate score - self.logger.log('Max new candidate score', max_new_score, iteration, color='green') - print_color(f"Iter {iteration}: Generated {len(iteration_new_scores)} new candidates. Avg score: {avg_new_score:.4f}, Max: {max_new_score:.4f}", 'cyan') - - # Update UCB scores and track metrics - self._update_buffer_ucb_scores() - - if self.buffer: - best_in_buffer = self._get_best_candidate_from_buffer(self.buffer) - if best_in_buffer: - best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1) - metrics['best_candidate_scores'].append(best_score) - else: - metrics['best_candidate_scores'].append(-np.inf) - metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) - metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) - - # Logging - if iteration % log_frequency == 0: - self.logger.log('Best candidate score', best_score, iteration, color='green') - self.logger.log('Buffer size', len(self.buffer), iteration, color='blue') - self.logger.log('Buffer average score', metrics['buffer_avg_score'][-1], iteration, color='cyan') - self.logger.log('Total samples', total_samples, iteration, color='yellow') - self.logger.log('Total proposals', self.total_proposals, iteration, color='red') - print_color(f"Log @ Iter {iteration}: Best score: {best_score:.4f}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') - - # Test evaluation (same as parent) - if test_dataset is not None and iteration % eval_frequency == 0: - try: - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_candidate: - print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') - continue - self.optimizer.update(best_candidate['params']) - - test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=self.min_score, num_threads=num_threads, - description=f"Evaluating best candidate (iteration {iteration})") - - self.optimizer.update(current_params) - self.logger.log('Test score', test_score, iteration, color='green') - except Exception as e: - print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') - - # Save agent (same as parent) - if save_frequency is not None and iteration % save_frequency == 0: - try: - best_overall_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_overall_candidate: - print_color(f"Iter {iteration}: No valid candidate for agent save.", 'yellow') - continue - self.optimizer.update(best_overall_candidate['params']) - self.save_agent(save_path, iteration) - print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') - except Exception as e: - print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') - - except Exception as e: - print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') - self.logger.log('Iteration error', str(e), iteration, color='red') - continue - - # End of search (same as parent) - print_color("Parallel UCB search finished.", 'blue') - - final_iteration = num_search_iterations - self.logger.log('Parallel UCB search completed', final_iteration, final_iteration, color='blue') - self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') - - if not self.buffer: - print_color("Buffer is empty at the end of search. No best candidate found.", 'red') - return metrics, -np.inf - - final_best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not final_best_candidate: - print_color("No valid candidate found at the end of search.", 'red') - return metrics, -np.inf - final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) - - self.logger.log('Final best score', final_best_score, final_iteration, color='green') - print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') - - # Load best parameters into the agent - self.optimizer.update(final_best_candidate['params']) - - return metrics, float(final_best_score) - - -class HybridUCB_LLM(MinibatchAlgorithm): - """ - UCB Search Algorithm with Function Approximation (LLM). - - Keeps a buffer of candidates. - In each iteration: - - With probability alpha: - 1. Picks a candidate 'a' from the buffer with the highest UCB score. - 2. Updates the optimizer with 'a's parameters. - 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a_prime'. - 4. Evaluates 'a_prime' on a validation set minibatch. - 5. Updates statistics of 'a' (based on the training minibatch). - 6. Adds 'a_prime' (with its validation stats) to the buffer. - - With probability 1-alpha: - 1. Uses an external LLM, prompted with candidates from the buffer, to generate a new candidate 'a_prime'. - 2. Evaluates 'a_prime' on a validation set minibatch. - 3. Adds 'a_prime' (with its validation stats) to the buffer. - If the buffer is full, evicts the candidate with the lowest UCB score. - """ - - def __init__(self, - agent: trace.Module, - optimizer, - max_buffer_size: int = 10, - ucb_exploration_factor: float = 0.3, - alpha: float = 0.3, - llm_model: str = None, - num_samples_in_prompt: int = 5, - logger=None, - num_threads: int = None, - *args, - **kwargs): - super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) - - self.alpha = alpha - self.llm_model = llm_model - self.num_samples_in_prompt = num_samples_in_prompt - self.llm_prompt_budget_factor = 0.5 - - self.buffer = deque(maxlen=max_buffer_size) - self.max_buffer_size = max_buffer_size - self.ucb_exploration_factor = ucb_exploration_factor - - if not hasattr(self.optimizer, 'step'): - raise ValueError("Optimizer must have a 'step' method.") - - self._total_evaluations_tracker = 0 - - # Initialize LLM - self.llm = LLM(model=self.llm_model) - print_color(f"Initialized HybridUCB_LLM with alpha={self.alpha}, LLM model={self.llm_model}", "cyan") - - def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: - """Sample a minibatch from the dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos'): - print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') - return [], [] - - dataset_size = len(dataset['inputs']) - if dataset_size == 0: - print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') - return [], [] - - actual_batch_size = min(batch_size, dataset_size) - indices = np.random.choice(dataset_size, actual_batch_size, replace=False) - xs = [dataset['inputs'][i] for i in indices] - infos = [dataset['infos'][i] for i in indices] - return xs, infos - - def _evaluate_candidate(self, - params_to_eval_dict: Dict[str, Any], - dataset: Dict[str, List[Any]], - guide, - evaluation_batch_size: int, - num_threads: Optional[int] = None - ) -> Tuple[float, int]: - """Evaluates a given set of parameters on samples from the provided dataset.""" - if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: - print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') - return -np.inf, 0 - - original_params_backup = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - - try: - self.optimizer.update(params_to_eval_dict) - except Exception as e: - print_color(f"Error updating agent with params_to_eval_dict: {e}. Using current agent state for eval.", "red") - - eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) - - if not eval_xs: - print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') - self.optimizer.update(original_params_backup) - return -np.inf, 0 - - eval_scores = evaluate(self.agent, - guide, - eval_xs, - eval_infos, - min_score=self.min_score if hasattr(self, 'min_score') else None, - num_threads=num_threads or self.num_threads, - description=f"Evaluating candidate") - - self.optimizer.update(original_params_backup) - - avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else 0 - eval_count = len(eval_xs) - - return float(avg_score), eval_count - - def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates UCB score for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('inf') - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - if total_tracked_evaluations == 0: - total_tracked_evaluations = 1 - - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations + 1e-9) / candidate_buffer_entry['eval_count']) - - return mean_score + exploration_term - - def _calculate_lcb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: - """Calculates Lower Confidence Bound for a candidate in the buffer.""" - if candidate_buffer_entry['eval_count'] == 0: - return float('-inf') # Unvisited states get lowest bound - - mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - - # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall - # and to ensure log argument is > 0. - # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. - if total_tracked_evaluations == 0: # Should not happen if we init with one eval - total_tracked_evaluations = 1 - - # LCB exploration term: ucb_exploration_factor scales the confidence interval - # Higher factor = more exploration, lower factor = more exploitation - exploration_term = self.ucb_exploration_factor * \ - math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) - - return mean_score - exploration_term - - def _update_buffer_ucb_scores(self): - """Recalculates and updates UCB scores for all candidates in the buffer.""" - if not self.buffer: - return - - for candidate_entry in self.buffer: - candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) - - def _get_best_candidate_from_buffer(self, buffer): - """Get the best candidate from buffer, excluding those with eval_count = 0.""" - if not buffer: - return None - - # Filter out candidates with eval_count = 0 - valid_candidates = [c for c in buffer if c['eval_count'] > 0] - if not valid_candidates: - # If no candidates have been evaluated, return the one with highest UCB score - return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) - return max(valid_candidates, key=lambda c: c['score_sum'] / c['eval_count']) - - def print_intervals(self, buffer): - """Print confidence intervals for debugging in the form of open intervals (LCB, UCB)""" - print_color("Confidence intervals for all candidates:", 'cyan') - for i, candidate_entry in enumerate(buffer): - lcb = self._calculate_lcb(candidate_entry, self._total_evaluations_tracker) - ucb = candidate_entry['ucb_score'] - mean_score = candidate_entry['score_sum'] / (candidate_entry['eval_count'] or 1) - eval_count = candidate_entry['eval_count'] - - # Format as open interval (LCB, UCB) with mean score and evaluation count - interval_str = f"Action {i+1}: ({lcb:.4f}, {ucb:.4f}) [mean: {mean_score:.4f}, n: {eval_count}]" - print_color(interval_str, 'cyan') - - def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: - """ - Prompts an LLM with current buffer candidates to generate new string values for parameters. - Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. - """ - print_color("Attempting to generate candidate using LLM...", "blue") - if not self.buffer: - print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") - return None - - sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) - # Include first, last, and evenly spaced middle candidates - if len(sorted_buffer) <= self.num_samples_in_prompt: - prompt_candidates = sorted_buffer - elif self.num_samples_in_prompt <= 2: - # If only 1-2 samples requested, take first and optionally last - prompt_candidates = sorted_buffer[:self.num_samples_in_prompt] - else: - # Take first, last, and evenly spaced middle candidates - prompt_candidates = [sorted_buffer[0]] # First (highest UCB) - if self.num_samples_in_prompt > 2: - # Calculate indices for middle candidates - middle_count = self.num_samples_in_prompt - 2 # Exclude first and last - if middle_count > 0 and len(sorted_buffer) > 2: - # Evenly space middle candidates between index 1 and len-2 - middle_indices = [int(1 + i * (len(sorted_buffer) - 2) / (middle_count + 1)) - for i in range(1, middle_count + 1)] - prompt_candidates.extend([sorted_buffer[i] for i in middle_indices]) - prompt_candidates.append(sorted_buffer[-1]) # Last (lowest UCB) - - serializable_candidate_summaries = [] - for cand_entry in prompt_candidates: - summary = { - "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, - "eval_count": cand_entry['eval_count'], - "ucb_score": round(cand_entry.get('ucb_score',0), 4), - } - serializable_candidate_summaries.append(summary) - - example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} - - prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} - ] - - print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - response_format = {"type": "json_object"} - llm_response = self.llm(prompt_messages, response_format=response_format) - llm_response_str = llm_response.choices[0].message.content - - if not llm_response_str: - print_color("LLM returned an empty response.", "red") - return None - - cleaned_llm_response_str = llm_response_str.strip() - - try: - llm_params_raw = json.loads(cleaned_llm_response_str) - except json.JSONDecodeError as e: - print_color(f"JSON parsing attempts failed: {e}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - if not isinstance(llm_params_raw, dict): - print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - candidate_params_dict = self.construct_update_dict(llm_params_raw) - return candidate_params_dict - - def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" - update_dict = {} - for node in self.agent.parameters(): - if node.trainable and node.py_name in suggestion: - try: - formatted_suggestion = suggestion[node.py_name] - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - if getattr(self, 'ignore_extraction_error', False): - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e - return update_dict - - def train(self, - guide, - train_dataset: Dict[str, List[Any]], - *, - num_search_iterations: int = 100, - validation_dataset: Dict[str, List[Any]] = None, - test_dataset: Dict[str, List[Any]] = None, - train_batch_size: int = 5, - evaluation_batch_size: int = 5, - eval_frequency: int = 1, - log_frequency: Optional[int] = None, - save_frequency: Optional[int] = None, - save_path: str = "checkpoints/ucb_llm_agent.pkl", - min_score_for_agent_update: Optional[float] = None, - verbose: Union[bool, str] = False, - num_threads: Optional[int] = None, - print_confidence_interval: bool = True, - **kwargs - ) -> Tuple[Dict[str, Any], float]: - - if validation_dataset is None: - validation_dataset = train_dataset - if test_dataset is None: - test_dataset = train_dataset - - num_threads = num_threads or self.num_threads - log_frequency = log_frequency or eval_frequency - self.min_score = min_score_for_agent_update - total_samples = 0 - self.total_proposals = 0 - - metrics = { - 'best_candidate_scores': [], - 'selected_action_ucb': [], - 'new_candidate_scores': [], - 'buffer_avg_score': [], - 'buffer_avg_evals': [], - 'llm_generation_failures': 0, - 'generation_path': [] - } - - # Initial candidate evaluation - print_color("Evaluating initial parameters using train_dataset samples...", 'cyan') - initial_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - - initial_score, initial_evals = self._evaluate_candidate( - initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += initial_evals - total_samples += initial_evals - - initial_candidate_entry = { - 'params': initial_params_dict, - 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, - 'eval_count': initial_evals, - 'ucb_score': 0.0, - 'iteration_created': 0 - } - self.buffer.append(initial_candidate_entry) - self._update_buffer_ucb_scores() - print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') - - # Log initial evaluation - self.logger.log('Initial UCB score', initial_score, 0, color='blue') - self.logger.log('Total samples', total_samples, 0, color='cyan') - self.logger.log('Total proposals', self.total_proposals, 0, color='red') - - # Main search loop - for iteration in range(1, num_search_iterations + 1): - try: - if not self.buffer: - print_color("Buffer is empty, stopping search.", 'red') - break - - self._update_buffer_ucb_scores() - a_prime_params_dict = None - a_prime_score = 0 - a_prime_evals = 0 - generation_method = "none" - if print_confidence_interval: - self.print_intervals(self.buffer) - - if iteration<=2 or random.random() < self.alpha: # UCB Path, for the first 2 iterations, we always use UCB because the buffer size is small, it's hard for LLM to generate good candidates - generation_method = "ucb" - metrics['generation_path'].append("ucb") - if not self.buffer: - print_color(f"Iter {iteration} (UCB Path): Buffer empty, cannot select action. Skipping.", "red") - continue - - action_candidate_a = self.select(self.buffer) - - selected_mean_score = action_candidate_a['score_sum'] / action_candidate_a['eval_count'] if action_candidate_a['eval_count'] > 0 else -np.inf - print_color(f"Iter {iteration} (UCB Path): Selected action candidate (UCB: {action_candidate_a['ucb_score']:.4f}, MeanScore: {selected_mean_score:.4f} Evals: {action_candidate_a['eval_count']})", 'blue') - # metrics['selected_action_ucb'].append(action_candidate_a['ucb_score']) - - # Log selected action UCB score - # self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') - # self.logger.log('Selected action mean score', selected_mean_score, iteration, color='cyan') - - self.optimizer.update(action_candidate_a['params']) - - train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) - if not train_xs: - print_color(f"Iter {iteration} (UCB Path): Training minibatch empty, skipping optimizer step.", 'yellow') - continue - - total_samples += len(train_xs) - - # Forward pass for 'a' - outputs_for_a = [] - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - outputs_for_a = async_run([self.forward]*len(train_xs), - [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], - max_workers=num_threads, - description=f"Iter {iteration} (UCB): Forward for 'a'") - else: - outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] - - scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] - for target, score, feedback in outputs_for_a: - scores_from_train.append(score) - targets_from_train.append(target) - feedbacks_from_train.append(feedback) - - if not scores_from_train: - print_color(f"Iter {iteration} (UCB Path): No outputs from forward pass for 'a'. Skipping.", 'yellow') - continue - - target_for_a = batchify(*targets_from_train) - feedback_for_a = batchify(*feedbacks_from_train).data - score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else 0 - - self.optimizer.zero_feedback() - self.optimizer.backward(target_for_a, feedback_for_a) - - # Get a_prime by optimizer step - try: - returned_params = self.optimizer.step(bypassing=True, verbose=False) - if not isinstance(returned_params, dict) or not returned_params: - print_color(f"Iter {iteration} (UCB Path): Optimizer.step did not return a valid param dict for a_prime. Using current agent params.", 'yellow') - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} - else: - a_prime_params_dict = {p: copy.deepcopy(p.data) for p in returned_params} - self.total_proposals += 1 - - except Exception as e: - print_color(f"Iter {iteration} (UCB Path): Error during optimizer.step for a_prime: {e}. Skipping.", 'red') - continue - - # Evaluate 'a' and 'a_prime' on validation set in parallel (like UCBSearchAlgorithm) - use_asyncio = self._use_asyncio(num_threads) - if use_asyncio: - evaluation_results = async_run( - [self._evaluate_candidate, self._evaluate_candidate], - [ - (action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads), - (a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads) - ], - max_workers=2, - description=f"Iter {iteration} (UCB): Parallel evaluation of 'a' and 'a_prime'" - ) - (a_score, a_evals), (a_prime_score, a_prime_evals) = evaluation_results - else: - a_score, a_evals = self._evaluate_candidate( - action_candidate_a['params'], validation_dataset, guide, evaluation_batch_size, num_threads - ) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - - self._total_evaluations_tracker += a_evals + a_prime_evals - total_samples += a_evals + a_prime_evals - - # Update stats of action_candidate_a - if score_for_a_on_train_batch > -np.inf: - action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) - action_candidate_a['eval_count'] += len(train_xs) - self._total_evaluations_tracker += len(train_xs) - - # Update stats with validation evaluation of 'a' - action_candidate_a['score_sum'] += a_score * a_evals - action_candidate_a['eval_count'] += a_evals - - print_color(f"Iter {iteration} (UCB Path): New candidate a_prime (from UCB) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') - self.logger.log('New candidate score', a_prime_score, iteration, color='green') - self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') - else: # LLM Pathcandi - generation_method = "llm" - metrics['generation_path'].append("llm") - print_color(f"Iter {iteration} (LLM Path): Generating candidate via LLM.", 'blue') - a_prime_params_dict = self._llm_generate_candidate() - - if a_prime_params_dict: - # Evaluate a_prime (from LLM path) - a_prime_score, a_prime_evals = self._evaluate_candidate( - a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads - ) - self._total_evaluations_tracker += a_prime_evals - total_samples += a_prime_evals - self.total_proposals += 1 - print_color(f"Iter {iteration} (LLM Path): New candidate a_prime (from LLM) generated. Eval Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') - self.logger.log('New candidate score', a_prime_score, iteration, color='green') #average new candidate score - else: - print_color(f"Iter {iteration} (LLM Path): LLM failed to generate a valid candidate. Skipping addition to buffer.", 'red') - metrics['llm_generation_failures'] += 1 - continue - - # Common logic for adding a_prime to buffer - metrics['new_candidate_scores'].append(a_prime_score) - - if a_prime_params_dict and a_prime_score > -np.inf and a_prime_evals > 0: - new_candidate_entry = { - 'params': a_prime_params_dict, - 'score_sum': a_prime_score * a_prime_evals, - 'eval_count': a_prime_evals, - 'ucb_score': 0.0, - 'iteration_created': iteration - } - - if len(self.buffer) == self.max_buffer_size: - self._update_buffer_ucb_scores() - candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) - self.buffer.remove(candidate_to_evict) - evicted_mean_score = candidate_to_evict['score_sum'] / candidate_to_evict['eval_count'] if candidate_to_evict['eval_count'] > 0 else -np.inf - print_color(f"Iter {iteration}: Buffer full. Evicted candidate (UCB: {candidate_to_evict['ucb_score']:.4f}, MeanScore: {evicted_mean_score:.4f})", 'magenta') - - self.buffer.append(new_candidate_entry) - print_color(f"Iter {iteration}: Added new candidate (from {generation_method}) to buffer.", 'magenta') - elif a_prime_params_dict: - print_color(f"Iter {iteration}: New candidate a_prime (from {generation_method}) had invalid score/evals ({a_prime_score}, {a_prime_evals}), not added to buffer.", 'yellow') - - self._update_buffer_ucb_scores() - - # Logging - if self.buffer: - best_in_buffer = max(self.buffer, key=lambda c: (c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1))) - current_best_score = best_in_buffer['score_sum']/(best_in_buffer['eval_count'] if best_in_buffer['eval_count'] > 0 else 1) - metrics['best_candidate_scores'].append(current_best_score) - - valid_scores = [c['score_sum']/(c['eval_count'] if c['eval_count'] > 0 else 1) for c in self.buffer if c['eval_count'] > 0] - metrics['buffer_avg_score'].append(np.mean(valid_scores) if valid_scores else -np.inf) - metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) - else: - metrics['best_candidate_scores'].append(0) - metrics['buffer_avg_score'].append(0) - metrics['buffer_avg_evals'].append(0) - - if iteration % log_frequency == 0: - log_data = { - "iteration": iteration, - "best_score": metrics['best_candidate_scores'][-1], - "newly_evaluated_candidate_score": a_prime_score, - "buffer_size": len(self.buffer), - "buffer_avg_score": metrics['buffer_avg_score'][-1], - "buffer_avg_evals": metrics['buffer_avg_evals'][-1], - "total_evaluations_ucb_T": self._total_evaluations_tracker, - "total_samples": total_samples, - "generation_method_this_iter": generation_method, - "llm_generation_total_failures": metrics['llm_generation_failures'] - } - if generation_method == "ucb" and metrics['selected_action_ucb']: - log_data["selected_action_ucb"] = metrics['selected_action_ucb'][-1] - - # Log all important metrics - self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') - self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') - self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') - self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') - self.logger.log('Total samples', log_data['total_samples'], iteration, color='yellow') - self.logger.log('Total proposals', self.total_proposals, iteration, color='red') - - print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Gen method: {generation_method}, Buffer size: {len(self.buffer)}, Total samples: {total_samples}", 'green') - - if test_dataset is not None and iteration % eval_frequency == 0: - try: - # Save current agent parameters - current_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - - # Find the best candidate in the buffer (highest mean score) - best_candidate = self._get_best_candidate_from_buffer(self.buffer) - if not best_candidate: - print_color(f"Iter {iteration}: No valid candidate for test evaluation.", 'yellow') - continue - - # Load best candidate's parameters into the agent for evaluation - self.optimizer.update(best_candidate['params']) - - # Evaluate the best candidate on test set - test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=self.min_score, num_threads=num_threads, - description=f"Evaluating best candidate (iteration {iteration})") - - # Restore original agent parameters - self.optimizer.update(current_params) - - self.logger.log('Test score', test_score, iteration, color='green') - except Exception as e: - print_color(f"Iter {iteration}: Test evaluation failed: {e}", 'red') - - if save_frequency is not None and iteration % save_frequency == 0 and self.buffer: - try: - best_overall_candidate_entry = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) - self.optimizer.update(best_overall_candidate_entry['params']) - if hasattr(self, 'save_agent'): - self.save_agent(save_path, iteration) - best_mean_score_for_save = best_overall_candidate_entry['score_sum'] / (best_overall_candidate_entry['eval_count'] if best_overall_candidate_entry['eval_count'] > 0 else 1E-9) - print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer (Mean Score: {best_mean_score_for_save:.4f}).", 'green') - else: - print_color(f"Iter {iteration}: save_agent method not found, skipping save.", 'yellow') - except Exception as e: - print_color(f"Iter {iteration}: Agent save failed: {e}", 'red') - - except Exception as e: - print_color(f"Iter {iteration}: Iteration failed with error: {e}. Skipping to next iteration.", 'red') - self.logger.log('Iteration error', str(e), iteration, color='red') - continue - - print_color("UCB-LLM search finished.", 'blue') - - final_best_candidate = max(self.buffer, key=lambda c: (c['score_sum'] / (c['eval_count'] if c['eval_count'] > 0 else 1E-9))) - final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] if final_best_candidate['eval_count'] > 0 else 1E-9) - final_best_evals = final_best_candidate['eval_count'] - print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_evals}", 'green') - - self.optimizer.update(final_best_candidate['params']) - - return metrics, float(final_best_score) - - def select(self, buffer): - '''Selects candidate with highest UCB score.''' - if not buffer: return None - return max(buffer, key=lambda c: c.get('ucb_score', -float('inf'))) - - -class UCBSearchFunctionApproximationAlgorithm(UCBSearchAlgorithm): - """ - UCB Search Algorithm that uses LLM function approximation to select candidates. - """ - - def __init__(self, llm_model,num_samples_in_prompt:int=5, *args, **kwargs): - super().__init__(*args, **kwargs) - self.llm_model = llm_model - self.llm = LLM(model=self.llm_model) - self.num_samples_in_prompt = num_samples_in_prompt - print_color(f"Initialized UCBSearchFunctionApproximationAlgorithm with LLM model={self.llm_model}", "cyan") - - def select(self, buffer): - """Generate a new candidate entry using LLM. Note: this doesn't add it to the buffer.""" - new_action_params = self._llm_generate_candidate() - new_candidate_entry = { - 'params': new_action_params, - 'score_sum': 0, - 'eval_count': 0, - 'ucb_score': 0.0, - 'iteration_created': 0 - } - return new_candidate_entry - - def _llm_generate_candidate(self) -> Optional[Dict[trace.nodes.ParameterNode, str]]: - """ - Prompts an LLM with current buffer candidates to generate new string values for parameters. - Returns a dictionary mapping ParameterNode objects to new string values, or None on failure. - """ - print_color("Attempting to generate candidate using LLM...", "blue") - if not self.buffer: - print_color("LLM generation: Buffer is empty, cannot provide context to LLM.", "yellow") - return None - - sorted_buffer = sorted(list(self.buffer), key=lambda c: c.get('ucb_score', -float('inf')), reverse=True) - # Include first, last, and evenly spaced middle candidates - if len(sorted_buffer) <= self.num_samples_in_prompt: - prompt_candidates = sorted_buffer - elif self.num_samples_in_prompt <= 2: - # If only 1-2 samples requested, take first and optionally last - prompt_candidates = sorted_buffer[:self.num_samples_in_prompt] - else: - # Take first, last, and evenly spaced middle candidates - prompt_candidates = [sorted_buffer[0]] # First (highest UCB) - if self.num_samples_in_prompt > 2: - # Calculate indices for middle candidates - middle_count = self.num_samples_in_prompt - 2 # Exclude first and last - if middle_count > 0 and len(sorted_buffer) > 2: - # Evenly space middle candidates between index 1 and len-2 - middle_indices = [int(1 + i * (len(sorted_buffer) - 2) / (middle_count + 1)) - for i in range(1, middle_count + 1)] - prompt_candidates.extend([sorted_buffer[i] for i in middle_indices]) - prompt_candidates.append(sorted_buffer[-1]) # Last (lowest UCB) - - serializable_candidate_summaries = [] - for cand_entry in prompt_candidates: - summary = { - "parameters": {getattr(p,'py_name'): copy.deepcopy(p.data) for p in cand_entry['params']}, - "eval_count": cand_entry['eval_count'], - "ucb_score": round(cand_entry.get('ucb_score',0), 4), - } - serializable_candidate_summaries.append(summary) - - example_param_structure_json_str = {getattr(p,'py_name'): copy.deepcopy(p.data) for p in self.agent.parameters()} - - prompt_messages = [ - {"role": "system", "content": "You are an expert in model optimization. Your task is to propose new string values for model parameters with high UCB scores. Please output ONLY a valid JSON dictionary where keys are parameter names and values are the new string values for those parameters, matching the example structure provided. Do not add any explanations or markdown formatting around the JSON."}, - {"role": "user", "content": f"Here are some current candidates from the search buffer and their statistics:\\n{serializable_candidate_summaries}\\n\\nHere is an example of the required JSON output structure (parameter names as keys, new string values as values):\\n{example_param_structure_json_str}\\n\\nPlease generate a new set of parameters in exactly the same JSON format. Make sure use double quotes for the keys and values."} - ] - - print_color(f"LLM prompt (summary): {len(prompt_candidates)} candidates, structure example provided.", "magenta") - response_format = {"type": "json_object"} - llm_response = self.llm(prompt_messages, response_format=response_format) - llm_response_str = llm_response.choices[0].message.content - - if not llm_response_str: - print_color("LLM returned an empty response.", "red") - return None - - cleaned_llm_response_str = llm_response_str.strip() - - try: - llm_params_raw = json.loads(cleaned_llm_response_str) - self.total_proposals += 1 - except json.JSONDecodeError as e: - print_color(f"JSON parsing attempts failed: {e}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - if not isinstance(llm_params_raw, dict): - print_color(f"LLM output was not a JSON dictionary after parsing: {type(llm_params_raw)}", "red") - print_color("Returning the candidate with the highest UCB score in the buffer.", "red") - return max(self.buffer, key=lambda c: c.get('ucb_score', -float('inf')))['params'] - - candidate_params_dict = self.construct_update_dict(llm_params_raw) - return candidate_params_dict - - def construct_update_dict(self, suggestion: Dict[str, Any]) -> Dict[ParameterNode, Any]: - """Convert the suggestion in text into the right data type.""" - update_dict = {} - for node in self.agent.parameters(): - if node.trainable and node.py_name in suggestion: - try: - formatted_suggestion = suggestion[node.py_name] - if type(formatted_suggestion) == str and 'def' in formatted_suggestion: - formatted_suggestion = format_str(formatted_suggestion, mode=FileMode()) - update_dict[node] = type(node.data)(formatted_suggestion) - except (ValueError, KeyError) as e: - if getattr(self, 'ignore_extraction_error', False): - warnings.warn( - f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type" - ) - else: - raise e - return update_dict From e97ebb40efe09ed2562ccafc116378d57e824fb8 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 22:35:12 -0400 Subject: [PATCH 105/172] add updated XML parsing, add fix to ProblemInstance --- opto/optimizers/optoprime_v2.py | 148 ++++++++++-------- .../unit_tests/test_optimizer_xml_parsing.py | 3 +- 2 files changed, 88 insertions(+), 63 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index bf0a3de7..d0094779 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -120,64 +120,13 @@ def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", value_block = extract_first_top_level_block(var_block, value_tag) # Only add if both name and value tags are present and name is non-empty after stripping if name_block is not None and value_block is not None: - var_name = strip_nested_blocks(name_block, name_tag).strip() + var_name = name_block.strip() var_value = value_block.strip() if value_block is not None else '' if var_name: # Only require name to be non-empty, value can be empty result['variables'][var_name] = var_value return result -@dataclass -class ProblemInstance: - instruction: str - code: str - documentation: str - variables: str - inputs: str - others: str - outputs: str - feedback: str - - problem_template = dedent( - """ - # Instruction - {instruction} - - # Code - {code} - - # Documentation - {documentation} - - # Variables - {variables} - - # Inputs - {inputs} - - # Others - {others} - - # Outputs - {outputs} - - # Feedback - {feedback} - """ - ) - - def __repr__(self) -> str: - return self.problem_template.format( - instruction=self.instruction, - code=self.code, - documentation=self.documentation, - variables=self.variables, - inputs=self.inputs, - outputs=self.outputs, - others=self.others, - feedback=self.feedback, - ) - class OptimizerPromptSymbolSet: """ @@ -225,6 +174,19 @@ def output_response_extractor(self, response: str) -> Dict[str, Any]: else: raise NotImplementedError( "If you supplied a custom output format prompt template, you need to implement your own response extractor") + + @property + def default_prompt_symbols(self) -> Dict[str, str]: + return { + "variables": self.variables_section_title, + "inputs": self.inputs_section_title, + "outputs": self.outputs_section_title, + "others": self.others_section_title, + "feedback": self.feedback_section_title, + "instruction": self.instruction_section_title, + "code": self.code_section_title, + "documentation": self.documentation_section_title, + } class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): @@ -249,6 +211,77 @@ class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): value_tag = "data" +@dataclass +class ProblemInstance: + instruction: str + code: str + documentation: str + variables: str + inputs: str + others: str + outputs: str + feedback: str + + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet + + problem_template = dedent( + """ + # Instruction + {instruction} + + # Code + {code} + + # Documentation + {documentation} + + # Variables + {variables} + + # Inputs + {inputs} + + # Others + {others} + + # Outputs + {outputs} + + # Feedback + {feedback} + """ + ) + + def __repr__(self) -> str: + return self.replace_symbols(self.problem_template.format( + instruction=self.instruction, + code=self.code, + documentation=self.documentation, + variables=self.variables, + inputs=self.inputs, + outputs=self.outputs, + others=self.others, + feedback=self.feedback, + ), self.optimizer_prompt_symbol_set.default_prompt_symbols) + + def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: + default_prompt_symbols = { + "variables": "# Variables", + "constraints": "# Constraints", + "inputs": "# Inputs", + "outputs": "# Outputs", + "others": "# Others", + "feedback": "# Feedback", + "instruction": "# Instruction", + "code": "# Code", + "documentation": "# Documentation", + } + + for k, v in symbols.items(): + text = text.replace(default_prompt_symbols[k], v) + return text + + # TODO: solution1 -> solution2 -> solution3 # TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 # TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases @@ -413,16 +446,7 @@ def __init__( self.summary_log = [] if log else None self.memory = FIFOBuffer(memory_size) - self.default_prompt_symbols = { - "variables": self.optimizer_prompt_symbol_set.variables_section_title, - "inputs": self.optimizer_prompt_symbol_set.inputs_section_title, - "outputs": self.optimizer_prompt_symbol_set.outputs_section_title, - "others": self.optimizer_prompt_symbol_set.others_section_title, - "feedback": self.optimizer_prompt_symbol_set.feedback_section_title, - "instruction": self.optimizer_prompt_symbol_set.instruction_section_title, - "code": self.optimizer_prompt_symbol_set.code_section_title, - "documentation": self.optimizer_prompt_symbol_set.documentation_section_title, - } + self.default_prompt_symbols = self.optimizer_prompt_symbol_set.default_prompt_symbols self.prompt_symbols = copy.deepcopy(self.default_prompt_symbols) self.initialize_prompt() diff --git a/tests/unit_tests/test_optimizer_xml_parsing.py b/tests/unit_tests/test_optimizer_xml_parsing.py index edbb3758..e66658b0 100644 --- a/tests/unit_tests/test_optimizer_xml_parsing.py +++ b/tests/unit_tests/test_optimizer_xml_parsing.py @@ -30,6 +30,7 @@ - No reasoning/variable tags scenarios """ + class TestXMLParsing(unittest.TestCase): def test_basic_parsing(self): @@ -104,7 +105,7 @@ def test_nested_name_tags(self): expected = { 'reasoning': 'Reasoning here', 'variables': { - 'outer_name': 'some_value' + 'inner_name\n outer_name': 'some_value' } } self.assertEqual(result, expected) From 47cb1fee10bd5dd035f74482b8e4f0a152f0ea85 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 10 Jul 2025 23:43:31 -0400 Subject: [PATCH 106/172] fix error --- opto/optimizers/optoprime_v2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index d0094779..afdd2cd3 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -626,6 +626,7 @@ def problem_instance(self, summary, mask=None): constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Others" not in mask else "" ), feedback=summary.user_feedback if "#Feedback" not in mask else "", + optimizer_prompt_symbol_set=self.optimizer_prompt_symbol_set ) def _step( From 7a9f829a6438fc0dd8767f65a9dcbc11194eee16 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 11 Jul 2025 00:28:32 -0400 Subject: [PATCH 107/172] add fix to examples --- opto/optimizers/optoprime_v2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index afdd2cd3..843249af 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -567,7 +567,8 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): # Add examples if len(self.memory) > 0: - prefix = user_prompt.split(self.final_prompt)[0] + formatted_final = self.final_prompt.format(names=var_names) + prefix = user_prompt.split(formatted_final)[0] examples = [] for variables, feedback in self.memory: examples.append( @@ -583,7 +584,7 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): user_prompt = ( prefix + f"\nBelow are some variables and their feedbacks you received in the past.\n\n{examples}\n\n" - + self.final_prompt + + formatted_final ) self.memory.add((summary.variables, summary.user_feedback)) From 04eded60a4e24cb0bb5598c5d89ead72c1935a2c Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 11 Jul 2025 17:03:04 -0400 Subject: [PATCH 108/172] fix the `_step()` where the suggestion is now nested --- opto/optimizers/optoprime_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 843249af..62a1c0e0 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -157,7 +157,6 @@ class OptimizerPromptSymbolSet: reasoning_tag = "reasoning" improved_variable_tag = "variable" name_tag = "name" - value_tag = "value" # custom output format (this will give the highest degree of freedom) # once it's set, it will override the default output format @@ -651,7 +650,8 @@ def _step( return {} suggestion = self.extract_llm_suggestion(response) - update_dict = self.construct_update_dict(suggestion) + update_dict = self.construct_update_dict(suggestion['variables']) + # suggestion has two keys: reasoning, and variables if self.log is not None: self.log.append( From 02ad4ed9e79c1b14a8f20364d11878d890c2f86d Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 17:52:49 +0000 Subject: [PATCH 109/172] Update copy test --- tests/unit_tests/test_copy.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_copy.py b/tests/unit_tests/test_copy.py index 3f361fef..fa772183 100644 --- a/tests/unit_tests/test_copy.py +++ b/tests/unit_tests/test_copy.py @@ -9,7 +9,14 @@ def test_deepcopy_plain_node(): x = trace.node("x") # should not raise - copy.deepcopy(x) + y = copy.deepcopy(x) + + assert y.name == x.py_name + '_copy:0' + + z = copy.deepcopy(y) + + assert z.name == y.py_name + '_copy:0' + def test_deepcopy_fun_parameter(): From 36545818bc5e62a51d7c668eb9df14e356b64c59 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 17:53:32 +0000 Subject: [PATCH 110/172] Add a runnable search algorithm implementation. --- opto/trainer/algorithms/search_algorithms.py | 890 +++++++++++++++++++ 1 file changed, 890 insertions(+) create mode 100644 opto/trainer/algorithms/search_algorithms.py diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py new file mode 100644 index 00000000..4c3a08af --- /dev/null +++ b/opto/trainer/algorithms/search_algorithms.py @@ -0,0 +1,890 @@ +import numpy as np +import copy +import heapq +from dataclasses import dataclass +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trace.nodes import ParameterNode +from opto.trainer.utils import async_run, batch_run +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify, standard_forward +from opto.trainer.evaluators import evaluate +from opto.trainer.loader import DataLoader + + +# TODO save and load SearchAlgorithm +# TODO async version +# TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate +# TODO a better data structure to store samples + +# update_dict + +# Some helper function to convert between trace.Module and update_dict + + +def standard_forward(agent, x, guide, info, min_score=0): + """ Forward and compute feedback. + + Args: + agent: trace.Module + x: input + guide: (question, student_answer, info) -> score, feedback + info: additional information for the guide + min_score: minimum score when exception happens + + Returns: + target: output of the agent + score: score from the guide + feedback: feedback from the guide + """ + try: + target = agent(x) + score, feedback = guide(x, target.data, info) + except trace.ExecutionError as e: + target = e.exception_node + score, feedback = min_score, target.create_feedback('full') + return target, score, feedback +def is_node_copy(a, b): + # check if a is a copy of b or b is a copy of a + # For int:0, its deepcopied version is int0_copy:x + """ Check if a is a copy of b or b is a copy of a or if they are the same node.""" + if a.name == b.name: + return True + if '_copy' in a.name and (a.name.split(':')[0].replace('_copy', '') == b.py_name): + return True + if '_copy' in b.name and (b.name.split(':')[0].replace('_copy', '') == a.py_name): + return True + return False + +def is_module_copy(a, b): + """ Check if a and b (trace.Modules) are copies of each other. """ + parameters_a = a.parameters() + parameters_b = b.parameters() + # Check if all parameters of a are copies of b or vice versa + for p_a in parameters_a: + if not any(is_node_copy(p_a, p_b) for p_b in parameters_b): + return False + for p_b in parameters_b: + if not any(is_node_copy(p_b, p_a) for p_a in parameters_a): + return False + return True + +def remap_update_dict(base_module, update_dict): + """ Remap the update dict to the agent's parameters. update_dict might have keys which are copies of the base_module's parameters or visa versa. + This function remaps the keys in update_dict to the original parameters of the base_module. + + The return dict is empty if no keys in update_dict matched any parameters of the base_module. This condition can be used to check if the update_dict contains non-trivial updates. + """ + parameters = base_module.parameters() # get the parameters of the base agent + remapped_update_dict = {} + for k, v in update_dict.items(): + for p in parameters: + # Check if k is a copy of p or p is a copy of k + if is_node_copy(k, p): + k = p # remap k to the original parameter + remapped_update_dict[k] = v # set the value in the remapped update dict + break # stop checking once we've found a match + # remapped_update_dict is empty if no keys in update_dict matched any parameters of the base_module + return remapped_update_dict + +def set_module_parameters(agent, update_dict): + """ Set the parameters of the agent based on the update_dict. + The update_dict is a dictionary of ParameterNode: value pairs. + The agent's parameters will be updated with the values from the update_dict. + """ + remap_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters + for k, v in remap_update_dict.items(): + k._data = v # set the parameter's data to the value in the update_dict + +def create_module_from_update_dict(agent, update_dict): + """ Create a new agent from the update_dict. + The update_dict is a dictionary of ParameterNode: value pairs. + A new agent will be created with the parameters set to the values from the update_dict. + """ + new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent + set_module_parameters(new_agent, update_dict) # set the parameters of the new agent + return new_agent # return the new agent + + +# a1, a2, a3, a4 +# x1, x2, x3, x4 +# a11 (x1, x2) +# a12 (x3, x4) +# a21 (x1, x2) +# a22 (x3, x4) + +# N agents, M inputs +# N x M + +# A list (size len(agents)) with list of samples (size batchsize) for each agent, +# where each sample is a dict containing: +# - 'module': the trace.Module (proposal) +# - 'x': the input data +# - 'info': additional information about the input +# - 'target': the target output (if applicable) +# - 'score': the score of the proposal +# - 'feedback': the feedback from the guide + +#TODO naming +@dataclass +class Rollout: + """ A rollout is a single sample from the environment. It contains the module, input, info, target, score, and feedback. + This is used to store the results of the agent's evaluation on a single input. + """ + module: trace.Module # the trace.Module (proposal) + x: Any # the input data + info: Any # additional information about the input + target: trace.Node # the target output (if applicable) + score: float # the score of the proposal + feedback: Any # the feedback from the guide + + def to_dict(self): + """ Convert the rollout to a dictionary representation. """ + return { + "module": self.module, + "x": self.x, + "info": self.info, + "target": self.target.data, + "score": self.score, + "feedback": self.feedback, + } + +class Subgraph: + """ A subgraph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. + """ + module: trace.Module # the trace.Module (proposal) that generated the rollouts + rollouts: List[Rollout] # a list of Rollout objects generated by the module on different inputs + def __init__(self, rollouts): + """ Initialize a subgraph with the given rollouts. """ + # Check that all rollouts have the same module + if not all(rollouts[0].module == r.module for r in rollouts): + raise ValueError("All rollouts must have the same module.") + self.module = rollouts[0].module # the module is the same for all rollouts + self.rollouts = rollouts + + def get_scores(self): + """ Get the scores of the rollouts in the subgraph. """ + return [r.score for r in self.rollouts] + + def __len__(self): + """ Get the number of rollouts in the subgraph. """ + return len(self.rollouts) + + def __iter__(self): + """ Iterate over the rollouts in the subgraph. """ + return iter(self.rollouts) + + def extend(self, other): + """ Extend the subgraph with another subgraph. """ + if not isinstance(other, Subgraph): + raise ValueError("Can only extend with another Subgraph.") + if self.module != other.module: + raise ValueError("Cannot extend with a subgraph with a different module.") + self.rollouts.extend(other.rollouts) + + def to_list(self): + """ Convert the subgraph to a list of rollouts. """ + return [r.to_dict() for r in self.rollouts] + + + + + +# # TODO general broadcast decorator +# def broadcast_forward(num_threads=1, description=None, sub_batch_size=None): +# """ A decorator to broadcast the agents, xs, infos, and guides. + +# forward should be a function that takes the arguments in the following order: +# agent: trace.Module, the agent to evaluate +# x: input, the input to the agent +# info: additional information for each input +# guide: a single guide or a list of guides that provide feedback on the outputs +# min_score: float, minimum score when exception happens +# **kwargs: additional keyword arguments to pass to the forward function +# Returns: +# A wrapper function that takes agents, xs, infos, guides, min_score, and additional keyword arguments. +# The wrapper function will broadcast the agents, inputs, infos, and guides. + +# agents is expected to be a list of trace.Modules representing the agents. +# xs and infos are expected to be lists of the same length of batch size. +# guide can be a single guide or a list of guides of the same length as the number of agents. + +# The return of the wrapper function is a list of Subgraph objects, where each Subgraph contains a list of Rollout objects. +# """ + +# def decorator(forward): +# """ A decorator to broadcast the agents, inputs, infos, and guides. """ +# def wrapper(agents, xs, infos, guides, min_score=0., **kwargs): +# """ A wrapper to broadcast the agents, inputs, infos, and guides to match the batch size. """ + +# # Example: +# # agents : a1, a2 +# # inputs: x1, x2, x3 +# # infos: i1, i2, i3 +# # sub_batch_size: 2 + +# # The forward is called in this order: +# # (a1, x1, i1, guide1), +# # (a1, x2, i2, guide1), +# # (deepcopy(a1), x3, i3, guide1) +# # (a2, x1, i1, guide2), +# # (a2, x2, i2, guide2), +# # (deepcopy(a2), x3, i3, guide2) + + +# batch_size = len(xs) +# n_agents = len(agents) +# assert len(infos) == batch_size, "Length of infos must match length of xs." + + +# # broadcasted_agents = [proposal for proposal in agents for _ in range(batch_size)] # [a1, a1, a2, a2, ...] + +# # Broadcast the agents to match the batch size +# # [a1, a1, a1, a1, a1, ..., a2, a2, a2, ...] if sub_batch_size is not specified +# # [a1, a1, a1_copy_1, a1_copy_1, a1_copy_2, ..., a2, a2, a2_copy_1, ...] if sub_batch_size of 2 is specified +# sub_batch_size = sub_batch_size or batch_size # if sub_batch_size is not provided, use the batch size +# broadcasted_agents = [] +# for agent in agents: +# for i in range(batch_size): +# if i % sub_batch_size == 0 and i > 0: +# agent = copy.deepcopy(agent) # create a copy of the agent for the next sub-batch +# broadcasted_agents.append(agent) + +# # broadcast the inputs and infos to match the number of agents +# # [x1, x2, x3, ..., x1, x2, x3, ...] +# broadcasted_xs = [x for _ in range(n_agents) for x in xs] +# broadcasted_infos = [info for _ in range(n_agents) for info in infos] + +# # Broadcast the guides to match the batch size +# if isinstance(guides, list): +# assert len(guides) == n_agents, "If guides is a list, its length must match the number of agents." +# # If multiple guides are provided, broadcast each guide to match the batch size +# broadcasted_guides = [guide for guide in guides for _ in range(batch_size)] +# else: # If a single guide is provided, broadcast it to match the batch size +# broadcasted_guides = [guides for _ in range(n_agents * batch_size)] + +# description = description or f"Evaluating {n_agents} agents on {batch_size} inputs" + +# # Forward the agent on the inputs and compute the feedback using the guide +# forward = batch_run(max_workers=num_threads, description=description)(forward) +# _outputs = forward(broadcasted_agents, +# broadcasted_xs, +# broadcasted_infos, +# broadcasted_guides, +# min_score=min_score, +# **kwargs) # guide will be broadcasted inside as well +# # return list of (target, score, feedback) + + +# return outputs + +# return wrapper +# return decorator + + +class SearchAlgorithm(AlgorithmBase): + """ This implements a generic template for search algorithm. """ + + def __init__(self, + agent, + optimizer, + num_threads: int = None, # maximum number of threads to use for parallel execution + logger=None, + *args, + **kwargs, + ): + super().__init__(agent, num_threads=num_threads, logger=logger, *args, **kwargs) + self.optimizer = optimizer + self.n_iters = 0 # number of iterations + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size for broadcasting the agents + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_guide = None, # guide to provide scores for the test set + # test_frequency: Union[int, None] = 1, # frequency of evaluation + eval_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + **kwargs + ): + + ## Setup + # TODO legacy notation + test_frequency = eval_frequency # use eval_frequency as test_frequency + + log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) + self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided + test_guide = test_guide or guide + self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input + self.score_range = score_range or (0., 1.) + # Underscore attributes are temporary attributes for the algorithm (which will not be saved) + # They would not affect the agent's state or the training process. + self._loader = DataLoader(train_dataset, batch_size=batch_size) # default data loader for training + self.sub_batch_size = sub_batch_size # sub-batch size for broadcasting the agents + self._guide = guide + self._validate_dataset = validate_dataset + self._validate_guide = validate_guide or guide + + # Evaluate the agent before learning + # NOTE set test_frequency < 0 to skip first evaluation + if (test_frequency is not None) and test_frequency > 0: + info_test = self.test(test_dataset, test_guide) + self.log(info_test) + + # Save the agent before learning if save_frequency > 0 + if (save_frequency is not None) and save_frequency > 0: + self.save(save_path) + + samples = None + self.n_epochs = 0 # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) + self.n_samples = 0 # number of training samples processed by the algorithm (This is incremented in sample) + train_scores = [] # to store the scores of the agent during training + + while self.n_epochs < num_epochs : + + print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") + + # 1. Propose new parameters given the current state of the algorithm + # proposals: list of trace.Modules + update_dict, proposals, info_update = self.update(samples, verbose=verbose, **kwargs) + self.optimizer.update(update_dict) # update the agent with the proposed parameters + + # 2. Get feedback on the proposed parameters on the current batch + # samples: list of list of dict(module, x, info, target, score, feedback) + samples, info_sample = self.sample(proposals, verbose=verbose, **kwargs) + + # Evaluate the agent after update + if (test_frequency is not None) and (self.n_iters % test_frequency == 0): + info_test = self.test(test_dataset, test_guide) + self.log(info_test, prefix="Test: ") + + # Save the algorithm state + if (save_frequency is not None and save_frequency > 0) and self.n_iters % save_frequency == 0: + self.save(save_path) + + # Log information + train_scores.append(info_sample['mean_score']) # so that mean can be computed + if self.n_iters % log_frequency == 0: + self.logger.log('Average mean score', np.mean(train_scores), self.n_iters, color='blue') + self.log(info_update, prefix="Update: ") + self.log(info_sample, prefix="Sample: ") + self.n_samples += sum(len(s) for s in samples) # update the number of samples processed + self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') + # Log parameters + for p in self.agent.parameters(): + self.logger.log(f"Parameter: {p.name}", p.data, self.n_iters, color='red') + + # Update counters + self.n_epochs = info_sample['n_epochs'] # update the number of epochs completed + self.n_iters += 1 + return + + # TODO + def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_threads=None, description=None): + """ Evaluate the agent on the given dataset. """ + num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, + description=description, num_samples=self.num_eval_samples) + if all([s is not None for s in test_scores]): + return np.mean(test_scores) + + # TODO move it out? + def sample(self, agents, loader=None, guide=None, **kwargs): + """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. + + Args: + agents (list): A list of trace.Modules (proposed parameters) to evaluate. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + list of list of dict: + A list (size len(agents)) with list of samples (size batchsize) for each agent, + where each sample is a dict containing: + - 'module': the trace.Module (proposal) + - 'x': the input data + - 'info': additional information about the input + - 'target': the target output (if applicable) + - 'score': the score of the proposal + - 'feedback': the feedback from the guide + + NOTE: The return might not be ordered in the same way as the agents. + """ + assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." + + loader = loader or self._loader # use the provided loader or the default one (train_dataset loader) + guide = guide or self._guide # use the provided guide or the default one (train_dataset guide) + + # Get a batch of inputs and infos from the loader + xs, infos = loader.sample() + + # XXX hack for now + self.xs, self.infos = xs, infos # store the inputs and infos for later use + + # Evaluate each agent on the sampled inputs + # + # agents : a1, a2 + # inputs: x1, x2, x3 + # infos: i1, i2, i3 + # sub_batch_size: 2 + # + # The forward is called in this order: + # (a1, x1, i1, guide1), + # (a1, x2, i2, guide1), + # (deepcopy(a1), x3, i3, guide1) + # (a2, x1, i1, guide2), + # (a2, x2, i2, guide2), + # (deepcopy(a2), x3, i3, guide2) + + num_threads = self.num_threads + min_score = self.score_range[0] + + batch_size = len(xs) + sub_batch_size = self.sub_batch_size or batch_size # if sub_batch_size is not provided, use the batch size + n_agents = len(agents) + + assert len(infos) == batch_size, "Length of infos must match length of xs." + + # Broadcast the agents to match the batch size + # [a1, a1, a1, a1, a1, ..., a2, a2, a2, ...] if sub_batch_size is not specified + # [a1, a1, a1_copy_1, a1_copy_1, a1_copy_2, ..., a2, a2, a2_copy_1, ...] if sub_batch_size of 2 is specified + broadcasted_agents = [] + for agent in agents: + for i in range(batch_size): + if i % sub_batch_size == 0 and i > 0: + agent = copy.deepcopy(agent) # create a copy of the agent for the next sub-batch + broadcasted_agents.append(agent) + + # Broadcast the inputs and infos to match the number of agents + # [x1, x2, x3, ..., x1, x2, x3, ...] + broadcasted_xs = [x for _ in range(n_agents) for x in xs] + broadcasted_infos = [info for _ in range(n_agents) for info in infos] + + # Broadcast the guides to match the batch size + + description = f"Forwarding {n_agents} agents on {batch_size} inputs" + + # Forward the agent on the inputs and compute the feedback using the guide + batched_forward = batch_run(max_workers=num_threads, description=description)(standard_forward) + outputs = batched_forward(agent=broadcasted_agents, + x=broadcasted_xs, + info=broadcasted_infos, + guide=guide, # guide will be broadcasted inside + min_score=min_score) + # return list of (target, score, feedback) + + # Collect results + results = [] # list of subgraphs (Subgraph objects) for each agent + for i in range(n_agents): + rollouts = [] # the compute result of each batch for a agent (trace.Module) + _agent = broadcasted_agents[i * batch_size ] # the first agent in the batch + for j in range(batch_size): + rollout = Rollout( + module=broadcasted_agents[i * batch_size + j], + x=broadcasted_xs[i * batch_size + j], + info=broadcasted_infos[i * batch_size + j], + target=outputs[i * batch_size + j][0], # target output + score=outputs[i * batch_size + j][1], # score of the proposal + feedback=outputs[i * batch_size + j][2], # feedback of the proposal + ) + if _agent != rollout.module: + results.append(Subgraph(rollouts)) # append the subgraph to the results + _agent = rollout.module # update the agent to the current one + rollouts = [] # reset rollouts for the new agent + rollouts.append(rollout) + + if rollouts: + results.append(Subgraph(rollouts)) # append the subgraph to the results + + # Log information about the sampling + log_info = { + 'mean_score': np.mean([ g.get_scores() for g in results]), + 'batch_size': batch_size, + 'sub_batch_size': sub_batch_size, + 'n_epochs': loader.n_epochs, + } + return results, log_info + + def log(self, info_log, prefix=""): + """ Log the information from the algorithm. """ + for key, value in info_log.items(): + try: + if value is not None: + self.logger.log(f"{prefix}{key}", value, self.n_iters) + except Exception as e: + print(e) + breakpoint() # if logging fails, we can debug here + + def test(self, test_dataset, guide): + min_score = self.score_range[0] + # Test the agent's performance + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=min_score, num_threads=self.num_threads, + description=f"Evaluating agent (iteration {self.n_iters})") # and log + return {'test_score': test_score} + + def save(self, save_path): + self.save_agent(save_path, self.n_iters) + # TODO save full state of self + + # Helper methods for the algorithm + def get_minibatch(self, samples): + """ Get a minibatch of samples from the provided samples. """ + # Since all proposals share the same batch, we can return the first sample's x and info + # return [s.x for s in samples[0]], [s['info'] for s in samples[0]] + return self.xs, self.infos # XXX hack for now + + # Unimplemented methods that should be implemented by subclasses + def update(self, samples=None, verbose=False, **kwargs): + """ Update the agent based on the provided samples. + Args: + samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. + proposals (list of trace.Module): A list of proposed parameters (trace.Module) after the update. + info_log (dict of str: Any): A dictionary containing logging information about the update process. + + This method updates the agent's parameters based on samples of the training dataset and validation dataset (provided by self.get_validate_dataset). + In addition, it return new agents (proposals) that can be used for collecting data for the next iteration. + """ + raise NotImplementedError("The update method should be implemented by subclasses.") + # return update_dict, proposals, info_log + + +class ModuleCandidate: + + def __init__(self, + base_module: Optional[trace.Module], + update_dict: Optional[Dict[ParameterNode, Any]] = None, + ): + """ A candidate module with its base module and update dictionary. + Args: + base_module (trace.Module): The base module to use as a template for the candidate. + update_dict (dict): A dictionary of ParameterNode: value pairs to update the base module; the key can be a deep copy of the base module's parameters. + stats (dict): A dictionary of statistics about the candidate. + """ + assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." + self.base_module = base_module + self.update_dict = update_dict if update_dict is not None else {} + self.rollouts = [] # list of dicts containing the rollout information + + def get_module(self): + """ Apply the update_dict to the base_module and return the updated module. This will not update the base_module itself.""" + return create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else self.base_module + + def apply_update(self, base_module=None): + """ Apply update to the base_module in place. """ + set_module_parameters(base_module or self.base_module, self.update_dict) + + def __deepcopy__(self, memo): + """ Create a deep copy, except for the base_module which is not copied, it is the original module. """ + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k != 'base_module': + setattr(result, k, deepcopy(v, memo)) + else: + setattr(result, k, v) # base_module is not copied, it is the original module + return result + + def __equal__(self, other): + """ Check if two candidates are equal based on their base_module and update_dict. """ + if not isinstance(other, ModuleCandidate): + return False + if self.base_module != other.base_module: + return False + update_dict_self = remap_update_dict(self.base_module, self.update_dict) + update_dict_other = remap_update_dict(other.base_module, other.update_dict) + return update_dict_self == update_dict_other + + def add_rollouts(self, rollouts: List[Dict[str, Any]]): + """ Add rollouts to the candidate. """ + + # # Convert all ParameterNode to data in the rollouts + # _rollouts = [] + # for r in rollouts: + # _r = {} + # for k, v in r.items(): + # if isinstance(v, trace.ParameterNode): + # _r[k] = v.data + # else: + # _r[k] = v + + # _rollouts.append(_r) # convert all ParameterNode to data + self.rollouts.extend(rollouts) + # # XXX TODO hacky + # self.rollouts.rollouts.extend(_rollouts) # extend the rollouts with the + + def score(self): + """ Compute the score of the candidate based on the rollouts. """ + if not self.rollouts: + return None + scores = [r['score'] for r in self.rollouts] + return np.mean(scores) if scores else None + +class PrioritySearch(SearchAlgorithm): + + # def train(self, *args, + # num_candidates: int = 10, # number of candidates to propose + # default_score: Union[float, None] = None, # default score for the candidates + # validate_proposals: bool = True, # whether to validate the proposed parameters # TODO better naming + # **kwargs + # ): + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size for broadcasting the agents + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose + default_score: Union[float, None] = None, # default score for the candidates + validate_proposals: bool = True, # whether to validate the proposed parameters + # Additional keyword arguments + **kwargs + ): + + + # Create agents and optimizers for search + self.num_candidates = num_candidates # number of candidates to propose + self.score_range = score_range or (0., 1.) # XXX hacky now + self.default_score = default_score if default_score is not None else self.score_range[0] # default score for the candidates + self.validate_proposals = validate_proposals # whether to validate the proposed parameters + self._queue = [(self.default_score, ModuleCandidate(self.agent))] # priority queue of ModuleCandidates, initialized with the base agent + + super().train(guide, train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + test_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + **kwargs) + + def update(self, samples=None, verbose=False, **kwargs): + + if samples is not None: + # 1. Propose new parameters based on running LLM optimizers on the collected samples + candidates = self.propose(samples, verbose=verbose, **kwargs) # List of ModuleCandidates + # 2. Validate the proposed parameters + validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue + # 3. Update the priority queue with the validation results + self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information + # 4. Explore and exploit the priority queue + best_candidate = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + exploration_candidates = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + + + # TBD Log information about the update + info_log = { + 'best_candidate_score': best_candidate.score(), + 'num_exploration_candidates': len(exploration_candidates), + } + return best_candidate.update_dict, [c.get_module() for c in exploration_candidates], info_log + + def propose(self, samples=None, verbose=False, n_proposals=1, **kwargs): + """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. + + Args: + samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. + n_proposals (int): Number of proposals to generate per optimizer. Defaults to 1. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + + Returns: + candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. + """ + if samples is None: + parameters = self.optimizer.parameters # use the current parameters of the optimizer + update_dict = {p: p.data for p in parameters} # return the current parameters as the update dict + # TODO what to do here? should we return n_proposals variations? + return [update_dict] # return the update dict as a list + + def _step(n, verbose=False, num_threads=None, **kwargs): + """ Standard optimizer step for a single agent. """ + # optimizer = self._optimizers[n] # get the optimizer for the n-th agent + # TODO this seems slow + optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one + + rollouts = samples[n] # Subgraph + + # Make sure all rollouts are based on the same module, so they can be viewed as a minibatch. + optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters + + targets = [r.target for r in rollouts] + feedbacks = [r.feedback for r in rollouts] + # batchify the targets and feedbacks + target = batchify(*targets) + feedback = batchify(*feedbacks).data # str + # standard optimizer step + optimizer.zero_feedback() # reset the optimizer's feedback + optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks + update_dict = optimizer.step(verbose=verbose, num_threads=num_threads, bypassing=True, **kwargs) + # the update_dict is linked to the copied parameters of the agent, we set it back to the agent's parameters + update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters + return update_dict # return the proposed parameters + + n_agents = len(samples) # number of agents + args_list = [(n, verbose, self.num_threads) for n in range(n_agents)] + args_list = args_list * n_proposals # repeat args_list n_proposals times + kwargs_list = [kwargs] * n_agents * n_proposals # repeat kwargs for each agent + update_dicts = async_run([_step]*n_agents*n_proposals, # run the optimizer step for each agent in parallel + args_list=args_list, + kwargs_list=kwargs_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description="Running optimizers on samples") + # update_dicts is a list of dicts of length n_agents * n_proposals + # Create ModuleCandidate objects for each proposed update_dict + candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts] + return candidates + + + def validate(self, candidates, samples=None, verbose=False, **kwargs): + """ Validate the proposed candidate parameters + Args: + candidates (list of dict): A list of ModuleCandidate objects representing the proposed parameters. + samples (list of dict, optional): A list of samples collected in the current iteration. Defaults to None. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + results (dict [ModuleCandidate, list of dict]): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + """ + + # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. + if self._validate_dataset is None: + # If no validation dataset is provided, use the current batch + xs, infos = self.get_minibatch(samples) + validate_dataset = {'inputs': xs, 'infos': infos} + else: + validate_dataset = self._validate_dataset + + + class Loader: # an trivial loader for the API + def __init__(self): + self.n_epochs = 0 + def sample(self): + return validate_dataset['inputs'], validate_dataset['infos'] + loader = Loader() # create a loader for the validation dataset + candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates + validate_samples, _ = self.sample(candidate_agents, loader=loader, guide=self._validate_guide, **kwargs) + # TODO log _ + + if self.validate_proposals: + if self._validate_dataset is None: + validate_samples += samples # if no validation dataset is provided, append the samples to the validate_samples + else: # validate the agents in the validate_dataset + # TODO need a flag? + exploration_agents = [rollouts.module for rollouts in samples] + exploration_samples = self.sample(exploration_agents, loader=loader, guide=self._validate_guide, **kwargs) + validate_samples += exploration_samples # append the exploration samples to the validate_samples + + + # Return a dict, key: ModuleCandidate, value: rollouts (list of dicts) + results = {} + for rollouts in validate_samples: + # rollouts is subgraph + agent = rollouts.module + index = candidate_agents.index(agent) + candidate = candidates[index] # get the candidate corresponding to the agent + # TODO delete 'module' from the rollouts dict? + if candidate in results: + # If the candidate already exists in results, we can append the rollouts to the existing list + results[candidate].extend(rollouts) + else: + # If the candidate does not exist in results, we create a new entry + results[candidate] = rollouts + return results + + + + def update_memory(self, validate_results, **kwargs): + + """ Update the priority queue with the validation results. + Args: + validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + for candidate, rollouts in validate_results.items(): + candidate.add_rollouts(rollouts.to_list()) # add the rollouts to the candidate + score = self.compute_score(candidate) # compute the score for the candidate + heapq.heappush(self._queue, (-score, candidate)) # add the candidate to the priority queue + + def explore(self, **kwargs): + """ Explore the parameter space and propose new candidates. + Args: + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. + proposal_update_dicts (list of dict): A list of proposed parameter updates (dict) for the next iteration. + """ + # pop top self.num_candidates candidates from the priority queue + top_candidates = [] + while len(top_candidates) < self.num_candidates and self._queue: + score, candidate = heapq.heappop(self._queue) + top_candidates.append(candidate) # add the candidate to the top candidates + return top_candidates + + + def exploit(self, **kwargs): + """ Exploit the best candidate from the priority queue. This method should not change the priority queue. + Args: + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + ModuleCandidate: The best candidate from the priority queue. + """ + # Right now, we just return the best candidate from the priority queue + # This function can be overridden by subclasses to implement a different exploitation strategy + if not self._queue: + raise ValueError("The priority queue is empty. Cannot exploit.") + best = min(self._queue) # (score, candidate) + return best[1] + + def compute_score(self, candidate): + # By default, we compute the mean score of the rollouts + # NOTE This function can be overridden by subclasses to compute a different score + scores = [r['score'] for r in candidate.rollouts] + default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates + + return np.mean(scores) if scores else self.default_score From a2f1fa16198f2d17e5b729b6df5520f284b2e05f Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 17:54:53 +0000 Subject: [PATCH 111/172] Add the example script. --- examples/gsm8k_search_algo.py | 95 +++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 examples/gsm8k_search_algo.py diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py new file mode 100644 index 00000000..832ec8e1 --- /dev/null +++ b/examples/gsm8k_search_algo.py @@ -0,0 +1,95 @@ +import datasets +import numpy as np +from opto import trace +from opto.utils.llm import LLM, LiteLLM +from opto.optimizers import OptoPrime +from opto.trainer.algorithms.search_algorithms import PrioritySearch as SearchAlgorithm +from opto.trainer.loggers import TensorboardLogger +from opto.trainer.guide import VerbalJudgeGuide +from typing import Any + + +@trace.model +class Learner: + """ A basic LLM agent. """ + + def __init__(self, system_prompt: str = "You're a helpful agent", + user_prompt_template: str = "Query: {message}", + llm: LLM = None): + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template) + self.llm = llm or LLM() + + @trace.bundle() + def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: + """Call the LLM model. + + Args: + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). + message: the input to the agent. It can be a query, a task, a code, etc. + Returns: + The response from the agent. + """ + + if '{message}' not in user_prompt_template: + raise ValueError("user_prompt_template must contain '{message}'") + + response = self.llm( + messages=[{"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt_template.format(message=message)}] + ) + return response.choices[0].message.content + + def forward(self, message: Any) -> Any: + """ Forward pass of the agent. """ + return self.model(self.system_prompt, self.user_prompt_template, message) + + +Guide = VerbalJudgeGuide +Logger = TensorboardLogger + + +def main(): + # set seed + seed = 42 + num_epochs = 1 + batch_size = 1 + eval_frequency = -1 + num_threads = 3 + verbose = True + teacher_model = None # use default model + student_model = None # use default model + optimizer_model = None # use default model + + np.random.seed(seed) + + # In this example, we use the GSM8K dataset, which is a dataset of math word problems. + # We will look the training error of the agent on a small portion of this dataset. + train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] + train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) + test_dataset = train_dataset + + agent = Learner(llm=LLM(student_model)) + guide = Guide(llm=LLM(teacher_model)) + optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) + logger = Logger(verbose=verbose) + # set use_json_object_format=False if LLM does not support JSON object format + + alg = SearchAlgorithm( + agent=agent, + optimizer=optimizer, + logger=logger) + + alg.train(guide, + train_dataset, + num_epochs=num_epochs, + batch_size=batch_size, + eval_frequency=eval_frequency, + test_dataset=test_dataset, + num_threads=num_threads, + verbose='output' if verbose else False) + + +if __name__ == "__main__": + main() From 7dae49aabbff3e520be6eeb69a15f884ed31f597 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 18:00:02 +0000 Subject: [PATCH 112/172] Fix a bug caused by the recent update to evaluate of Minibatch. --- opto/trainer/algorithms/basic_algorithms.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 8ec0eb4f..194bb1c9 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -84,6 +84,7 @@ def train(self, if eval_frequency > 0: test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=num_threads, + num_samples=self.num_eval_samples, description=f"Evaluating agent (iteration {self.n_iters})") # and log self.logger.log('Average test score', test_score, self.n_iters, color='green') @@ -123,6 +124,7 @@ def train(self, if test_dataset is not None and self.n_iters % eval_frequency == 0: test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=num_threads, + num_samples=self.num_eval_samples, description=f"Evaluating agent (iteration {self.n_iters})") # and log self.logger.log('Average test score', test_score, self.n_iters, color='green') @@ -146,10 +148,10 @@ def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_t """ Evaluate the agent on the given dataset. """ num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, - num_samples=num_samples, description=description, num_samples=self.num_eval_samples) + num_samples=num_samples, description=description) if all([s is not None for s in test_scores]): return np.mean(test_scores) - + def has_improvement(self, xs, guide, infos, current_score, current_outputs, backup_dict, threshold=0, num_threads=None, *args, **kwargs): # This function can be overridden by subclasses to implement their own improvement check. """ Check if the updated agent is improved compared to the current one. @@ -166,6 +168,7 @@ def has_improvement(self, xs, guide, infos, current_score, current_outputs, back num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads new_score = self.evaluate(self.agent, guide, xs, infos, num_threads=num_threads, description=f"Checking improvement (iteration {self.n_iters})", + num_samples=self.num_eval_samples, *args, **kwargs) # evaluate the updated agent if new_score is None or new_score <= current_score - threshold: print_color(f"Update rejected: Current score {current_score}, New score {new_score}", 'red') @@ -305,13 +308,13 @@ def validate(): # Generate different proposals step_kwargs = dict(bypassing=True, verbose='output' if verbose else False) # we don't print the inner full message step_kwargs.update(kwargs) # update with additional kwargs if provided - + # Use aysnc_run to run the optimizer_step in parallel - # NOTE optimizer_step is coupled via async_run + # NOTE optimizer_step is coupled via async_run update_dicts = async_run([super().optimizer_step]*self.num_proposals, kwargs_list=[step_kwargs] * self.num_proposals, max_workers=num_threads, - description=f"Generating {self.num_proposals} proposals") # async step + description=f"Generating {self.num_proposals} proposals") # async step # Validate the proposals candidates = [] backup_dict = {p: copy.deepcopy(p.data) for p in self.agent.parameters()} # backup the current value From 5f93cceefc8c5417734a79fd6634492c68f50346 Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 18:46:37 +0000 Subject: [PATCH 113/172] Refactor into Sampler --- opto/trace/sampler.py | 283 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 opto/trace/sampler.py diff --git a/opto/trace/sampler.py b/opto/trace/sampler.py new file mode 100644 index 00000000..b0c1bfae --- /dev/null +++ b/opto/trace/sampler.py @@ -0,0 +1,283 @@ +import numpy as np +import copy +import heapq +from dataclasses import dataclass +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trace.nodes import ParameterNode +from opto.trainer.utils import async_run, batch_run +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify +from opto.trainer.evaluators import evaluate +from opto.trainer.loader import DataLoader + +@dataclass +class Rollout: + """ A rollout is a single sample from the environment. It contains the module, input, info, target, score, and feedback. + This is used to store the results of the agent's evaluation on a single input. + """ + module: trace.Module # the trace.Module (proposal) + x: Any # the input data + info: Any # additional information about the input + target: trace.Node # the target output (if applicable) + score: float # the score of the proposal + feedback: Any # the feedback from the guide + + def to_dict(self): + """ Convert the rollout to a dictionary representation. """ + return { + "module": self.module, + "x": self.x, + "info": self.info, + "target": self.target.data, + "score": self.score, + "feedback": self.feedback, + } + +class RolloutsGraph: + """ A rollouts graph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. + """ + module: trace.Module # the trace.Module (proposal) that generated the rollouts + rollouts: List[Rollout] # a list of Rollout objects generated by the module on different inputs + def __init__(self, rollouts): + """ Initialize a rollouts graph with the given rollouts. """ + # Check that all rollouts have the same module + if not all(rollouts[0].module == r.module for r in rollouts): + raise ValueError("All rollouts must have the same module.") + self.module = rollouts[0].module # the module is the same for all rollouts + self.rollouts = rollouts + + def get_scores(self): + """ Get the scores of the rollouts in the subgraph. """ + return [r.score for r in self.rollouts] + + def __len__(self): + """ Get the number of rollouts in the subgraph. """ + return len(self.rollouts) + + def __iter__(self): + """ Iterate over the rollouts in the subgraph. """ + return iter(self.rollouts) + + def extend(self, other): + """ Extend the subgraph with another subgraph. """ + if not isinstance(other, RolloutsGraph): + raise ValueError("Can only extend with another RolloutsGraph.") + if self.module != other.module: + raise ValueError("Cannot extend with a subgraph with a different module.") + self.rollouts.extend(other.rollouts) + + def to_list(self): + """ Convert the subgraph to a list of rollouts. """ + return [r.to_dict() for r in self.rollouts] + + +@dataclass +class RolloutConfig: + module: trace.Module # the trace.Module (proposal) + xs: List[Any] # the input data + infos: List[Any] # additional information about the input + guide: Any # the guide to evaluate the proposals + + def __init__(self, + module: trace.Module, + xs: List[Any], + infos: List[Any], + guide: Any): + """ Initialize a rollout config with the given module, inputs, infos, and guide. """ + # check types + if not isinstance(module, trace.Module): + raise TypeError("module must be a trace.Module.") + if not isinstance(xs, list): + raise TypeError("xs must be a list.") + if not isinstance(infos, list): + raise TypeError("infos must be a list.") + if not isinstance(guide, trace.Module): + raise TypeError("guide must be a trace.Module.") + if len(xs) != len(infos): + raise ValueError("Length of xs must match length of infos.") + self.module = module + self.xs = xs + self.infos = infos + self.guide = guide + + +# TODO move it and refactor the trainer code +def standard_forward(agent, x, guide, info, min_score=0): + """ Forward and compute feedback. + + Args: + agent: trace.Module + x: input + guide: (question, student_answer, info) -> score, feedback + info: additional information for the guide + min_score: minimum score when exception happens + + Returns: + target: output of the agent + score: score from the guide + feedback: feedback from the guide + """ + try: + target = agent(x) + score, feedback = guide(x, target.data, info) + except trace.ExecutionError as e: + target = e.exception_node + score, feedback = min_score, target.create_feedback('full') + return target, score, feedback + + +def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, description="Sampling rollouts.") -> List[RolloutsGraph]: + """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. + + Args: + configs (List[RolloutConfig]): A list of RolloutConfig objects, each containing\ + - module: the trace.Module (proposal) to evaluate + - xs: a list of input data to evaluate the proposal on + - infos: a list of additional information about the inputs + - guide: the guide to evaluate the proposals + num_threads (int): Number of threads to use for sampling. + forward (callable, optional): A custom forward function to use instead of the default one + (standard_forward). If None, the default forward function is used. + min_score (float, optional): Minimum score to return when an exception occurs. If None, it defaults to 0. + description (str): Description to display in the progress bar. + Returns: + List[RolloutsGraph]: A list of RolloutsGraph objects, one for each config + """ + if forward is None: + forward = standard_forward + + # Forward the agent on the inputs and compute the feedback using the guide + batched_forward = batch_run(max_workers=num_threads, description=description)(forward) + + agents = [ config.module for config in configs for _ in range(len(config.xs)) ] # repeat each agent for each input + xs = [ x for config in configs for x in config.xs ] # flatten + infos = [ info for config in configs for info in config.infos ] # flatten + guides = [ config.guide for config in configs for _ in range(len(config.xs)) ] # repeat each guide for each input + + outputs = batched_forward(agent=agents, + x=xs, + info=infos, + guide=guides, # guide will be broadcasted inside + min_score=min_score) + + # Collect the results into a list of RolloutsGraph objects + results = [] # list of subgraphs (RolloutsGraph objects) for each agent + _index = 0 # to track the indices processed + for i in range(len(configs)): + rollouts = [] + _agent = configs[i].module # the first agent in the batch + for j in range(len(configs[i].xs)): + assert _agent == agents[_index], "Agent mismatch in the rollouts." + rollout = Rollout( + module=agents[_index], + x=xs[_index], + info=infos[_index], + target=outputs[_index][0], # target output + score=outputs[_index][1], # score of the proposal + feedback=outputs[_index][2], # feedback of the proposal + ) + _index += 1 # increment the index + rollouts.append(rollout) + results.append(RolloutsGraph(rollouts)) # append the subgraph to the results + return results + + + +class Sampler: + + def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=None, score_range=(-np.inf, np.inf)): + """ Initialize the sampler with a data loader and a guide. + + Args: + loader (DataLoader): The data loader to sample from. + guide (AutoGuide): The guide to evaluate the proposals. + num_threads (int): Number of threads to use for sampling. + sub_batch_size (int, optional): Size of the sub-batch to use for sampling. If None, uses the batch size. + score_range (tuple): The range of scores to consider valid. + """ + self._loader = loader + self._guide = guide + self.num_threads = num_threads + self.sub_batch_size = sub_batch_size + self.score_range = score_range + if forward is None: + self.forward = standard_forward + + def sample(self, agents): + """ Sample a batch of data from the loader and evaluate the agents. + + Args: + agents (list): A list of trace.Modules (proposed parameters) to evaluate. + **kwargs: Additional keyword arguments that may be used by the implementation. + + Returns: + batch (dict): + A dictionary containing the sampled inputs and infos, where: + - 'inputs': a list of inputs sampled from the loader + - 'infos': a list of additional information for each input + + samples (list of RolloutsGraph): + A list of RolloutsGraph objects, each containing the rollouts generated by the agents on the sampled inputs. + Each RolloutsGraph contains: + - 'module': the trace.Module (proposal) + - 'rollouts': a list of Rollout objects containing: + - 'x': the input data + - 'info': additional information about the input + - 'target': the target output (if applicable) + - 'score': the score of the proposal + - 'feedback': the feedback from the guide + + NOTE: The return might not be ordered in the same way as the agents. + """ + + assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." + + # Get a batch of inputs and infos from the loader + xs, infos = self._loader.sample() + batch = { + 'inputs': xs, + 'infos': infos + } + + # Evaluate each agent on the sampled inputs + # + # agents : a1, a2 + # inputs: x1, x2, x3 + # infos: i1, i2, i3 + # sub_batch_size: 2 + # + # The forward is called in this order: + # (a1, x1, i1, guide1), + # (a1, x2, i2, guide1), + # (deepcopy(a1), x3, i3, guide1) + # (a2, x1, i1, guide2), + # (a2, x2, i2, guide2), + # (deepcopy(a2), x3, i3, guide2) + + # Create rollout configs for each agent + batch_size = len(xs) + assert len(infos) == batch_size, "Length of infos must match length of xs." + configs = [] + for agent in agents: + _xs, _infos = [], [] + for i in range(batch_size): + if i % self.sub_batch_size == 0 and i > 0: + configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self._guide)) + # reset + agent = copy.deepcopy(agent) # create a deep copy of the agent for the next sub-batch + _xs, _infos = [], [] + _xs.append(xs[i]) + _infos.append(infos[i]) + if _xs: # if there are inputs in the sub-batch + configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self._guide)) + + # Sample rollouts using the configs + description = f"Sampling {len(agents)} agents on {batch_size} inputs" + samples = sample_rollouts(configs, + forward=self.forward, + num_threads=self.num_threads, + min_score=self.score_range[0], + description=description) + + return samples From 2be34afa5d3027cdf5cbee988ba0b8a5218ac72d Mon Sep 17 00:00:00 2001 From: xuanfeiren Date: Mon, 14 Jul 2025 14:57:58 -0500 Subject: [PATCH 114/172] Made is_node_copy function transitive --- opto/trainer/algorithms/search_algorithms.py | 28 +++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 4c3a08af..ddadb6cb 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -44,17 +44,25 @@ def standard_forward(agent, x, guide, info, min_score=0): target = e.exception_node score, feedback = min_score, target.create_feedback('full') return target, score, feedback + +def get_original_name(node): + """Extract the original name from a node, removing all _copy suffixes.""" + py_name = node.py_name # This removes colons: "param:0" -> "param0" + + # Find the first occurrence of "_copy" and remove it and everything after + copy_index = py_name.find('_copy') + if copy_index != -1: + return py_name[:copy_index] + else: + return py_name + def is_node_copy(a, b): - # check if a is a copy of b or b is a copy of a - # For int:0, its deepcopied version is int0_copy:x - """ Check if a is a copy of b or b is a copy of a or if they are the same node.""" - if a.name == b.name: - return True - if '_copy' in a.name and (a.name.split(':')[0].replace('_copy', '') == b.py_name): - return True - if '_copy' in b.name and (b.name.split(':')[0].replace('_copy', '') == a.py_name): - return True - return False + """Check if two nodes are copies of each other by comparing their original names. + + This function has transitivity: if A is a copy of B and B is a copy of C, + then A is also considered a copy of C. + """ + return get_original_name(a) == get_original_name(b) def is_module_copy(a, b): """ Check if a and b (trace.Modules) are copies of each other. """ From dc329e0ff5300aad11b29d1404a4b44cbae5526b Mon Sep 17 00:00:00 2001 From: chinganc Date: Mon, 14 Jul 2025 22:05:21 +0000 Subject: [PATCH 115/172] Finish a runnable refactor code. --- opto/trainer/algorithms/search_algorithms.py | 413 ++++--------------- opto/{trace => trainer}/sampler.py | 24 +- 2 files changed, 100 insertions(+), 337 deletions(-) rename opto/{trace => trainer}/sampler.py (94%) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 4c3a08af..0f2641f1 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -7,10 +7,11 @@ from opto.trace.nodes import ParameterNode from opto.trainer.utils import async_run, batch_run from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify, standard_forward +from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify from opto.trainer.evaluators import evaluate from opto.trainer.loader import DataLoader +from opto.trainer.sampler import Sampler, RolloutsGraph # TODO save and load SearchAlgorithm # TODO async version @@ -22,6 +23,7 @@ # Some helper function to convert between trace.Module and update_dict +# TODO move it and refactor the trainer code def standard_forward(agent, x, guide, info, min_score=0): """ Forward and compute feedback. @@ -44,6 +46,7 @@ def standard_forward(agent, x, guide, info, min_score=0): target = e.exception_node score, feedback = min_score, target.create_feedback('full') return target, score, feedback + def is_node_copy(a, b): # check if a is a copy of b or b is a copy of a # For int:0, its deepcopied version is int0_copy:x @@ -106,196 +109,50 @@ def create_module_from_update_dict(agent, update_dict): return new_agent # return the new agent -# a1, a2, a3, a4 -# x1, x2, x3, x4 -# a11 (x1, x2) -# a12 (x3, x4) -# a21 (x1, x2) -# a22 (x3, x4) -# N agents, M inputs -# N x M +class Samples: -# A list (size len(agents)) with list of samples (size batchsize) for each agent, -# where each sample is a dict containing: -# - 'module': the trace.Module (proposal) -# - 'x': the input data -# - 'info': additional information about the input -# - 'target': the target output (if applicable) -# - 'score': the score of the proposal -# - 'feedback': the feedback from the guide + samples: List[RolloutsGraph] + dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys -#TODO naming -@dataclass -class Rollout: - """ A rollout is a single sample from the environment. It contains the module, input, info, target, score, and feedback. - This is used to store the results of the agent's evaluation on a single input. - """ - module: trace.Module # the trace.Module (proposal) - x: Any # the input data - info: Any # additional information about the input - target: trace.Node # the target output (if applicable) - score: float # the score of the proposal - feedback: Any # the feedback from the guide - - def to_dict(self): - """ Convert the rollout to a dictionary representation. """ - return { - "module": self.module, - "x": self.x, - "info": self.info, - "target": self.target.data, - "score": self.score, - "feedback": self.feedback, - } + def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): + assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." + assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + assert isinstance(dataset, dict), "dataset must be a dict." + assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." -class Subgraph: - """ A subgraph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. - """ - module: trace.Module # the trace.Module (proposal) that generated the rollouts - rollouts: List[Rollout] # a list of Rollout objects generated by the module on different inputs - def __init__(self, rollouts): - """ Initialize a subgraph with the given rollouts. """ - # Check that all rollouts have the same module - if not all(rollouts[0].module == r.module for r in rollouts): - raise ValueError("All rollouts must have the same module.") - self.module = rollouts[0].module # the module is the same for all rollouts - self.rollouts = rollouts - - def get_scores(self): - """ Get the scores of the rollouts in the subgraph. """ - return [r.score for r in self.rollouts] + self.samples = samples + self.dataset = dataset # TODO this cannot be extracted from the samples in general - def __len__(self): - """ Get the number of rollouts in the subgraph. """ - return len(self.rollouts) + def add_samples(self, samples): + """ Add samples to the Samples object. """ + assert isinstance(samples, Samples), "samples must be an instance of Samples." + samples = samples.samples # extract the samples from the Samples object + assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." + assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + + # TODO assert xs and infos are in self.minibatch + # add a function to extract unique inputs and infos from the samples + + self.samples.extend(samples) + + def get_batch(self): + return self.dataset #['inputs'], self.minibatch['infos'] def __iter__(self): - """ Iterate over the rollouts in the subgraph. """ - return iter(self.rollouts) - - def extend(self, other): - """ Extend the subgraph with another subgraph. """ - if not isinstance(other, Subgraph): - raise ValueError("Can only extend with another Subgraph.") - if self.module != other.module: - raise ValueError("Cannot extend with a subgraph with a different module.") - self.rollouts.extend(other.rollouts) + """ Iterate over the samples. """ + return iter(self.samples) - def to_list(self): - """ Convert the subgraph to a list of rollouts. """ - return [r.to_dict() for r in self.rollouts] - - - - - -# # TODO general broadcast decorator -# def broadcast_forward(num_threads=1, description=None, sub_batch_size=None): -# """ A decorator to broadcast the agents, xs, infos, and guides. - -# forward should be a function that takes the arguments in the following order: -# agent: trace.Module, the agent to evaluate -# x: input, the input to the agent -# info: additional information for each input -# guide: a single guide or a list of guides that provide feedback on the outputs -# min_score: float, minimum score when exception happens -# **kwargs: additional keyword arguments to pass to the forward function -# Returns: -# A wrapper function that takes agents, xs, infos, guides, min_score, and additional keyword arguments. -# The wrapper function will broadcast the agents, inputs, infos, and guides. - -# agents is expected to be a list of trace.Modules representing the agents. -# xs and infos are expected to be lists of the same length of batch size. -# guide can be a single guide or a list of guides of the same length as the number of agents. - -# The return of the wrapper function is a list of Subgraph objects, where each Subgraph contains a list of Rollout objects. -# """ - -# def decorator(forward): -# """ A decorator to broadcast the agents, inputs, infos, and guides. """ -# def wrapper(agents, xs, infos, guides, min_score=0., **kwargs): -# """ A wrapper to broadcast the agents, inputs, infos, and guides to match the batch size. """ - -# # Example: -# # agents : a1, a2 -# # inputs: x1, x2, x3 -# # infos: i1, i2, i3 -# # sub_batch_size: 2 - -# # The forward is called in this order: -# # (a1, x1, i1, guide1), -# # (a1, x2, i2, guide1), -# # (deepcopy(a1), x3, i3, guide1) -# # (a2, x1, i1, guide2), -# # (a2, x2, i2, guide2), -# # (deepcopy(a2), x3, i3, guide2) - - -# batch_size = len(xs) -# n_agents = len(agents) -# assert len(infos) == batch_size, "Length of infos must match length of xs." - - -# # broadcasted_agents = [proposal for proposal in agents for _ in range(batch_size)] # [a1, a1, a2, a2, ...] - -# # Broadcast the agents to match the batch size -# # [a1, a1, a1, a1, a1, ..., a2, a2, a2, ...] if sub_batch_size is not specified -# # [a1, a1, a1_copy_1, a1_copy_1, a1_copy_2, ..., a2, a2, a2_copy_1, ...] if sub_batch_size of 2 is specified -# sub_batch_size = sub_batch_size or batch_size # if sub_batch_size is not provided, use the batch size -# broadcasted_agents = [] -# for agent in agents: -# for i in range(batch_size): -# if i % sub_batch_size == 0 and i > 0: -# agent = copy.deepcopy(agent) # create a copy of the agent for the next sub-batch -# broadcasted_agents.append(agent) - -# # broadcast the inputs and infos to match the number of agents -# # [x1, x2, x3, ..., x1, x2, x3, ...] -# broadcasted_xs = [x for _ in range(n_agents) for x in xs] -# broadcasted_infos = [info for _ in range(n_agents) for info in infos] - -# # Broadcast the guides to match the batch size -# if isinstance(guides, list): -# assert len(guides) == n_agents, "If guides is a list, its length must match the number of agents." -# # If multiple guides are provided, broadcast each guide to match the batch size -# broadcasted_guides = [guide for guide in guides for _ in range(batch_size)] -# else: # If a single guide is provided, broadcast it to match the batch size -# broadcasted_guides = [guides for _ in range(n_agents * batch_size)] - -# description = description or f"Evaluating {n_agents} agents on {batch_size} inputs" - -# # Forward the agent on the inputs and compute the feedback using the guide -# forward = batch_run(max_workers=num_threads, description=description)(forward) -# _outputs = forward(broadcasted_agents, -# broadcasted_xs, -# broadcasted_infos, -# broadcasted_guides, -# min_score=min_score, -# **kwargs) # guide will be broadcasted inside as well -# # return list of (target, score, feedback) - - -# return outputs + def __len__(self): + return len(self.samples) -# return wrapper -# return decorator -class SearchAlgorithm(AlgorithmBase): +#TODO naming +class SearchAlgorithm(Minibatch): + # This only uses __init__ and evaluate of Minibatch class. """ This implements a generic template for search algorithm. """ - def __init__(self, - agent, - optimizer, - num_threads: int = None, # maximum number of threads to use for parallel execution - logger=None, - *args, - **kwargs, - ): - super().__init__(agent, num_threads=num_threads, logger=logger, *args, **kwargs) - self.optimizer = optimizer - self.n_iters = 0 # number of iterations def train(self, guide, # guide to provide feedback @@ -314,8 +171,7 @@ def train(self, # evaluation test_dataset = None, # dataset of (x, info) pairs to evaluate the agent test_guide = None, # guide to provide scores for the test set - # test_frequency: Union[int, None] = 1, # frequency of evaluation - eval_frequency: Union[int, None] = 1, # frequency of evaluation + eval_frequency: Union[int, None] = 1, # frequency of evaluation num_eval_samples: int = 1, # number of samples to use to evaluate each input # logging log_frequency = None, # frequency of logging @@ -325,9 +181,8 @@ def train(self, ): ## Setup - # TODO legacy notation - test_frequency = eval_frequency # use eval_frequency as test_frequency + test_frequency = eval_frequency # use eval_frequency as test_frequency # TODO legacy notation log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided @@ -336,12 +191,29 @@ def train(self, self.score_range = score_range or (0., 1.) # Underscore attributes are temporary attributes for the algorithm (which will not be saved) # They would not affect the agent's state or the training process. - self._loader = DataLoader(train_dataset, batch_size=batch_size) # default data loader for training - self.sub_batch_size = sub_batch_size # sub-batch size for broadcasting the agents - self._guide = guide + # self._loader = DataLoader(train_dataset, batch_size=batch_size) # default data loader for training self._validate_dataset = validate_dataset self._validate_guide = validate_guide or guide + self.train_sampler = Sampler( + DataLoader(train_dataset, batch_size=batch_size), + guide, + num_threads=self.num_threads, + sub_batch_size=sub_batch_size, + score_range=self.score_range + ) + self.validate_sampler = Sampler( + DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), + validate_guide or guide, + num_threads=self.num_threads, + sub_batch_size=sub_batch_size, + score_range=self.score_range + ) + + + + + # Evaluate the agent before learning # NOTE set test_frequency < 0 to skip first evaluation if (test_frequency is not None) and test_frequency > 0: @@ -396,129 +268,22 @@ def train(self, self.n_iters += 1 return - # TODO - def evaluate(self, agent, guide, xs, infos, min_score=None, num_samples=1, num_threads=None, description=None): - """ Evaluate the agent on the given dataset. """ - num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads - test_scores = evaluate(agent, guide, xs, infos, min_score=min_score, num_threads=num_threads, - description=description, num_samples=self.num_eval_samples) - if all([s is not None for s in test_scores]): - return np.mean(test_scores) - - # TODO move it out? - def sample(self, agents, loader=None, guide=None, **kwargs): + # Can be overridden by subclasses to implement specific sampling strategies + def sample(self, agents, verbose=False, **kwargs): """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. Args: agents (list): A list of trace.Modules (proposed parameters) to evaluate. - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - list of list of dict: - A list (size len(agents)) with list of samples (size batchsize) for each agent, - where each sample is a dict containing: - - 'module': the trace.Module (proposal) - - 'x': the input data - - 'info': additional information about the input - - 'target': the target output (if applicable) - - 'score': the score of the proposal - - 'feedback': the feedback from the guide - - NOTE: The return might not be ordered in the same way as the agents. + **kwargs: Additional keyword arguments that may be used by the implementation. """ - assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." - - loader = loader or self._loader # use the provided loader or the default one (train_dataset loader) - guide = guide or self._guide # use the provided guide or the default one (train_dataset guide) - - # Get a batch of inputs and infos from the loader - xs, infos = loader.sample() - - # XXX hack for now - self.xs, self.infos = xs, infos # store the inputs and infos for later use - - # Evaluate each agent on the sampled inputs - # - # agents : a1, a2 - # inputs: x1, x2, x3 - # infos: i1, i2, i3 - # sub_batch_size: 2 - # - # The forward is called in this order: - # (a1, x1, i1, guide1), - # (a1, x2, i2, guide1), - # (deepcopy(a1), x3, i3, guide1) - # (a2, x1, i1, guide2), - # (a2, x2, i2, guide2), - # (deepcopy(a2), x3, i3, guide2) - - num_threads = self.num_threads - min_score = self.score_range[0] - - batch_size = len(xs) - sub_batch_size = self.sub_batch_size or batch_size # if sub_batch_size is not provided, use the batch size - n_agents = len(agents) - - assert len(infos) == batch_size, "Length of infos must match length of xs." - - # Broadcast the agents to match the batch size - # [a1, a1, a1, a1, a1, ..., a2, a2, a2, ...] if sub_batch_size is not specified - # [a1, a1, a1_copy_1, a1_copy_1, a1_copy_2, ..., a2, a2, a2_copy_1, ...] if sub_batch_size of 2 is specified - broadcasted_agents = [] - for agent in agents: - for i in range(batch_size): - if i % sub_batch_size == 0 and i > 0: - agent = copy.deepcopy(agent) # create a copy of the agent for the next sub-batch - broadcasted_agents.append(agent) - - # Broadcast the inputs and infos to match the number of agents - # [x1, x2, x3, ..., x1, x2, x3, ...] - broadcasted_xs = [x for _ in range(n_agents) for x in xs] - broadcasted_infos = [info for _ in range(n_agents) for info in infos] - - # Broadcast the guides to match the batch size - - description = f"Forwarding {n_agents} agents on {batch_size} inputs" - - # Forward the agent on the inputs and compute the feedback using the guide - batched_forward = batch_run(max_workers=num_threads, description=description)(standard_forward) - outputs = batched_forward(agent=broadcasted_agents, - x=broadcasted_xs, - info=broadcasted_infos, - guide=guide, # guide will be broadcasted inside - min_score=min_score) - # return list of (target, score, feedback) - - # Collect results - results = [] # list of subgraphs (Subgraph objects) for each agent - for i in range(n_agents): - rollouts = [] # the compute result of each batch for a agent (trace.Module) - _agent = broadcasted_agents[i * batch_size ] # the first agent in the batch - for j in range(batch_size): - rollout = Rollout( - module=broadcasted_agents[i * batch_size + j], - x=broadcasted_xs[i * batch_size + j], - info=broadcasted_infos[i * batch_size + j], - target=outputs[i * batch_size + j][0], # target output - score=outputs[i * batch_size + j][1], # score of the proposal - feedback=outputs[i * batch_size + j][2], # feedback of the proposal - ) - if _agent != rollout.module: - results.append(Subgraph(rollouts)) # append the subgraph to the results - _agent = rollout.module # update the agent to the current one - rollouts = [] # reset rollouts for the new agent - rollouts.append(rollout) - - if rollouts: - results.append(Subgraph(rollouts)) # append the subgraph to the results + samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch # Log information about the sampling log_info = { - 'mean_score': np.mean([ g.get_scores() for g in results]), - 'batch_size': batch_size, - 'sub_batch_size': sub_batch_size, - 'n_epochs': loader.n_epochs, + 'mean_score': np.mean([ g.get_scores() for g in samples.samples]), + 'n_epochs': self.train_sampler.loader.n_epochs, } - return results, log_info + return samples, log_info def log(self, info_log, prefix=""): """ Log the information from the algorithm. """ @@ -528,7 +293,6 @@ def log(self, info_log, prefix=""): self.logger.log(f"{prefix}{key}", value, self.n_iters) except Exception as e: print(e) - breakpoint() # if logging fails, we can debug here def test(self, test_dataset, guide): min_score = self.score_range[0] @@ -542,12 +306,6 @@ def save(self, save_path): self.save_agent(save_path, self.n_iters) # TODO save full state of self - # Helper methods for the algorithm - def get_minibatch(self, samples): - """ Get a minibatch of samples from the provided samples. """ - # Since all proposals share the same batch, we can return the first sample's x and info - # return [s.x for s in samples[0]], [s['info'] for s in samples[0]] - return self.xs, self.infos # XXX hack for now # Unimplemented methods that should be implemented by subclasses def update(self, samples=None, verbose=False, **kwargs): @@ -640,6 +398,7 @@ def score(self): scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None + class PrioritySearch(SearchAlgorithm): # def train(self, *args, @@ -742,13 +501,15 @@ def propose(self, samples=None, verbose=False, n_proposals=1, **kwargs): # TODO what to do here? should we return n_proposals variations? return [update_dict] # return the update dict as a list + assert isinstance(samples, Samples), "samples must be an instance of Samples." + samples = samples.samples def _step(n, verbose=False, num_threads=None, **kwargs): """ Standard optimizer step for a single agent. """ # optimizer = self._optimizers[n] # get the optimizer for the n-th agent # TODO this seems slow optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one - rollouts = samples[n] # Subgraph + rollouts = samples[n] # RolloutsGraph # Make sure all rollouts are based on the same module, so they can be viewed as a minibatch. optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters @@ -766,11 +527,11 @@ def _step(n, verbose=False, num_threads=None, **kwargs): update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters return update_dict # return the proposed parameters - n_agents = len(samples) # number of agents - args_list = [(n, verbose, self.num_threads) for n in range(n_agents)] + n_subgraphs = len(samples) # number of subgraphs (agents) in the samples + args_list = [(n, verbose, self.num_threads) for n in range(n_subgraphs)] args_list = args_list * n_proposals # repeat args_list n_proposals times - kwargs_list = [kwargs] * n_agents * n_proposals # repeat kwargs for each agent - update_dicts = async_run([_step]*n_agents*n_proposals, # run the optimizer step for each agent in parallel + kwargs_list = [kwargs] * n_subgraphs * n_proposals # repeat kwargs for each agent + update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, kwargs_list=kwargs_list, max_workers=self.num_threads, # use the number of threads specified in the class @@ -795,35 +556,27 @@ def validate(self, candidates, samples=None, verbose=False, **kwargs): # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. if self._validate_dataset is None: # If no validation dataset is provided, use the current batch - xs, infos = self.get_minibatch(samples) - validate_dataset = {'inputs': xs, 'infos': infos} - else: - validate_dataset = self._validate_dataset - - - class Loader: # an trivial loader for the API - def __init__(self): - self.n_epochs = 0 - def sample(self): - return validate_dataset['inputs'], validate_dataset['infos'] - loader = Loader() # create a loader for the validation dataset + validate_dataset = samples.get_batch() # get the batch of inputs and infos from the samples + self.validate_sampler.loader.dataset = validate_dataset # set the validation dataset in the sampler + self.validate_sampler.batch_size = len(validate_dataset['inputs']) # set the batch size to the number of inputs in the validation dataset + candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates - validate_samples, _ = self.sample(candidate_agents, loader=loader, guide=self._validate_guide, **kwargs) + validate_samples = Samples(*self.validate_sampler.sample(candidate_agents)) # list of RolloutsGraph objects # TODO log _ if self.validate_proposals: if self._validate_dataset is None: - validate_samples += samples # if no validation dataset is provided, append the samples to the validate_samples + validate_samples.add_samples(samples) # if no validation dataset is provided, append the samples to the validate_samples else: # validate the agents in the validate_dataset # TODO need a flag? - exploration_agents = [rollouts.module for rollouts in samples] - exploration_samples = self.sample(exploration_agents, loader=loader, guide=self._validate_guide, **kwargs) - validate_samples += exploration_samples # append the exploration samples to the validate_samples + exploration_agents = [rollouts.module for rollouts in samples.samples] + exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents)) # sample the exploration agents + validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples # Return a dict, key: ModuleCandidate, value: rollouts (list of dicts) results = {} - for rollouts in validate_samples: + for rollouts in validate_samples.samples: # rollouts is subgraph agent = rollouts.module index = candidate_agents.index(agent) @@ -851,6 +604,8 @@ def update_memory(self, validate_results, **kwargs): score = self.compute_score(candidate) # compute the score for the candidate heapq.heappush(self._queue, (-score, candidate)) # add the candidate to the priority queue + + #### def explore(self, **kwargs): """ Explore the parameter space and propose new candidates. Args: diff --git a/opto/trace/sampler.py b/opto/trainer/sampler.py similarity index 94% rename from opto/trace/sampler.py rename to opto/trainer/sampler.py index b0c1bfae..568575d0 100644 --- a/opto/trace/sampler.py +++ b/opto/trainer/sampler.py @@ -10,6 +10,7 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify from opto.trainer.evaluators import evaluate from opto.trainer.loader import DataLoader +from opto.trainer.guide import AutoGuide @dataclass class Rollout: @@ -34,6 +35,7 @@ def to_dict(self): "feedback": self.feedback, } + class RolloutsGraph: """ A rollouts graph is a collection of rollouts generated by the same agent (trace.Module) on different inputs. """ @@ -92,8 +94,8 @@ def __init__(self, raise TypeError("xs must be a list.") if not isinstance(infos, list): raise TypeError("infos must be a list.") - if not isinstance(guide, trace.Module): - raise TypeError("guide must be a trace.Module.") + if not isinstance(guide, AutoGuide): + raise TypeError("guide must be a AutoGuide.") if len(xs) != len(infos): raise ValueError("Length of xs must match length of infos.") self.module = module @@ -186,6 +188,8 @@ def sample_rollouts(configs, num_threads=1, forward=None, min_score=None, descri class Sampler: + """ A sampler that samples a batch of data from the loader and evaluates the agents on the sampled inputs. + """ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=None, score_range=(-np.inf, np.inf)): """ Initialize the sampler with a data loader and a guide. @@ -196,9 +200,13 @@ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=No sub_batch_size (int, optional): Size of the sub-batch to use for sampling. If None, uses the batch size. score_range (tuple): The range of scores to consider valid. """ - self._loader = loader - self._guide = guide + self.loader = loader + self.guide = guide self.num_threads = num_threads + if sub_batch_size is None: + sub_batch_size = loader.batch_size + else: + assert sub_batch_size <= loader.batch_size, "sub_batch_size must be less than or equal to the loader's batch size." self.sub_batch_size = sub_batch_size self.score_range = score_range if forward is None: @@ -234,7 +242,7 @@ def sample(self, agents): assert all(isinstance(a, trace.Module) for a in agents), "All agents must be trace.Modules." # Get a batch of inputs and infos from the loader - xs, infos = self._loader.sample() + xs, infos = self.loader.sample() batch = { 'inputs': xs, 'infos': infos @@ -263,14 +271,14 @@ def sample(self, agents): _xs, _infos = [], [] for i in range(batch_size): if i % self.sub_batch_size == 0 and i > 0: - configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self._guide)) + configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self.guide)) # reset agent = copy.deepcopy(agent) # create a deep copy of the agent for the next sub-batch _xs, _infos = [], [] _xs.append(xs[i]) _infos.append(infos[i]) if _xs: # if there are inputs in the sub-batch - configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self._guide)) + configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self.guide)) # Sample rollouts using the configs description = f"Sampling {len(agents)} agents on {batch_size} inputs" @@ -280,4 +288,4 @@ def sample(self, agents): min_score=self.score_range[0], description=description) - return samples + return samples, batch From 1a098c7ee21aac6a7bcfd9d184f74752f4994c5e Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 15 Jul 2025 03:29:07 +0000 Subject: [PATCH 116/172] Update PrioritySearch --- opto/trainer/algorithms/search_algorithms.py | 249 +++++++++---------- opto/trainer/sampler.py | 28 +++ 2 files changed, 143 insertions(+), 134 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index e301e824..2f23f9c9 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -13,8 +13,8 @@ from opto.trainer.sampler import Sampler, RolloutsGraph -# TODO save and load SearchAlgorithm -# TODO async version +# TODO save and load SearchTemplate +# TODO async version??? # TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate # TODO a better data structure to store samples @@ -23,31 +23,6 @@ # Some helper function to convert between trace.Module and update_dict -# TODO move it and refactor the trainer code -def standard_forward(agent, x, guide, info, min_score=0): - """ Forward and compute feedback. - - Args: - agent: trace.Module - x: input - guide: (question, student_answer, info) -> score, feedback - info: additional information for the guide - min_score: minimum score when exception happens - - Returns: - target: output of the agent - score: score from the guide - feedback: feedback from the guide - """ - try: - target = agent(x) - score, feedback = guide(x, target.data, info) - except trace.ExecutionError as e: - target = e.exception_node - score, feedback = min_score, target.create_feedback('full') - return target, score, feedback - - def get_original_name(node): """Extract the original name from a node, removing all _copy suffixes.""" py_name = node.py_name # This removes colons: "param:0" -> "param0" @@ -69,16 +44,20 @@ def is_node_copy(a, b): def is_module_copy(a, b): """ Check if a and b (trace.Modules) are copies of each other. """ - parameters_a = a.parameters() - parameters_b = b.parameters() + parameters_a = a.parameters() # list of ParameterNode + parameters_b = b.parameters() # list of ParameterNode # Check if all parameters of a are copies of b or vice versa + # This might over count + # need to check 1:1 correspondence + matched = [] for p_a in parameters_a: - if not any(is_node_copy(p_a, p_b) for p_b in parameters_b): - return False - for p_b in parameters_b: - if not any(is_node_copy(p_b, p_a) for p_a in parameters_a): - return False - return True + _matched = [] + for p_b in parameters_b: + _matched.append(is_node_copy(p_a, p_b)) + np.array(matched) + if np.all(np.sum(matched, axis=1) == 1) and np.all(np.sum(matched, axis=0) == 1): + return True + return False def remap_update_dict(base_module, update_dict): """ Remap the update dict to the agent's parameters. update_dict might have keys which are copies of the base_module's parameters or visa versa. @@ -119,6 +98,8 @@ def create_module_from_update_dict(agent, update_dict): class Samples: + """ A container for samples collected during the search algorithm. It contains a list of RolloutsGraph objects + and a dataset with inputs and infos which created the list of RolloutsGraph. """ samples: List[RolloutsGraph] dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys @@ -130,7 +111,7 @@ def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." self.samples = samples - self.dataset = dataset # TODO this cannot be extracted from the samples in general + self.dataset = dataset # NOTE this cannot be extracted from the samples in general? def add_samples(self, samples): """ Add samples to the Samples object. """ @@ -152,16 +133,14 @@ def __iter__(self): return iter(self.samples) def __len__(self): - return len(self.samples) + return sum(len(s) for s in self.samples) -#TODO naming -class SearchAlgorithm(Minibatch): +class SearchTemplate(Minibatch): # This only uses __init__ and evaluate of Minibatch class. """ This implements a generic template for search algorithm. """ - def train(self, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent @@ -190,18 +169,13 @@ def train(self, ## Setup - test_frequency = eval_frequency # use eval_frequency as test_frequency # TODO legacy notation + test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided test_guide = test_guide or guide self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input self.score_range = score_range or (0., 1.) - # Underscore attributes are temporary attributes for the algorithm (which will not be saved) - # They would not affect the agent's state or the training process. - # self._loader = DataLoader(train_dataset, batch_size=batch_size) # default data loader for training - self._validate_dataset = validate_dataset - self._validate_guide = validate_guide or guide self.train_sampler = Sampler( DataLoader(train_dataset, batch_size=batch_size), @@ -210,22 +184,19 @@ def train(self, sub_batch_size=sub_batch_size, score_range=self.score_range ) + self._validate_dataset = validate_dataset # if None, the current batch will be used for validation self.validate_sampler = Sampler( - DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), + DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), validate_guide or guide, num_threads=self.num_threads, - sub_batch_size=sub_batch_size, + sub_batch_size=None, # no sub-batch size for validation score_range=self.score_range ) - - - - # Evaluate the agent before learning # NOTE set test_frequency < 0 to skip first evaluation if (test_frequency is not None) and test_frequency > 0: - info_test = self.test(test_dataset, test_guide) + info_test = self.test(test_dataset, test_guide) # test self.agent self.log(info_test) # Save the agent before learning if save_frequency > 0 @@ -244,15 +215,15 @@ def train(self, # 1. Propose new parameters given the current state of the algorithm # proposals: list of trace.Modules update_dict, proposals, info_update = self.update(samples, verbose=verbose, **kwargs) - self.optimizer.update(update_dict) # update the agent with the proposed parameters + self.optimizer.update(update_dict) # update self.agent with the proposed parameters # 2. Get feedback on the proposed parameters on the current batch - # samples: list of list of dict(module, x, info, target, score, feedback) + # samples: Samples object containing the samples and the minibatch samples, info_sample = self.sample(proposals, verbose=verbose, **kwargs) # Evaluate the agent after update if (test_frequency is not None) and (self.n_iters % test_frequency == 0): - info_test = self.test(test_dataset, test_guide) + info_test = self.test(test_dataset, test_guide) # test self.agent self.log(info_test, prefix="Test: ") # Save the algorithm state @@ -260,12 +231,15 @@ def train(self, self.save(save_path) # Log information + assert 'mean_score' in info_sample, "info_sample must contain 'mean_score'." + assert 'n_epochs' in info_sample, "info_sample must contain 'n_epochs'." + train_scores.append(info_sample['mean_score']) # so that mean can be computed if self.n_iters % log_frequency == 0: - self.logger.log('Average mean score', np.mean(train_scores), self.n_iters, color='blue') + self.logger.log('Average train score', np.mean(train_scores), self.n_iters, color='blue') self.log(info_update, prefix="Update: ") self.log(info_sample, prefix="Sample: ") - self.n_samples += sum(len(s) for s in samples) # update the number of samples processed + self.n_samples += len(samples) # update the number of samples processed self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') # Log parameters for p in self.agent.parameters(): @@ -289,7 +263,7 @@ def sample(self, agents, verbose=False, **kwargs): # Log information about the sampling log_info = { 'mean_score': np.mean([ g.get_scores() for g in samples.samples]), - 'n_epochs': self.train_sampler.loader.n_epochs, + 'n_epochs': self.train_sampler.n_epochs, } return samples, log_info @@ -314,7 +288,6 @@ def save(self, save_path): self.save_agent(save_path, self.n_iters) # TODO save full state of self - # Unimplemented methods that should be implemented by subclasses def update(self, samples=None, verbose=False, **kwargs): """ Update the agent based on the provided samples. @@ -334,7 +307,9 @@ def update(self, samples=None, verbose=False, **kwargs): # return update_dict, proposals, info_log +# TODO make this hashable? class ModuleCandidate: + """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ def __init__(self, base_module: Optional[trace.Module], @@ -349,11 +324,13 @@ def __init__(self, assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." self.base_module = base_module self.update_dict = update_dict if update_dict is not None else {} - self.rollouts = [] # list of dicts containing the rollout information + self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) def get_module(self): """ Apply the update_dict to the base_module and return the updated module. This will not update the base_module itself.""" - return create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else self.base_module + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else self.base_module + module._ModuleCandidate_candidate_id = id(self) # set the id of the module to the id of the candidate; this is used to identify the candidate in the priority queue + return module # return the updated module def apply_update(self, base_module=None): """ Apply update to the base_module in place. """ @@ -371,10 +348,9 @@ def __deepcopy__(self, memo): setattr(result, k, v) # base_module is not copied, it is the original module return result - def __equal__(self, other): + def __eq__(self, other): """ Check if two candidates are equal based on their base_module and update_dict. """ - if not isinstance(other, ModuleCandidate): - return False + assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." if self.base_module != other.base_module: return False update_dict_self = remap_update_dict(self.base_module, self.update_dict) @@ -383,21 +359,13 @@ def __equal__(self, other): def add_rollouts(self, rollouts: List[Dict[str, Any]]): """ Add rollouts to the candidate. """ + assert isinstance(rollouts, list), "rollouts must be a list of dicts." + assert all(isinstance(r, dict) for r in rollouts), "All rollouts must be dicts." + # Each rollout is a dict with keys: 'module', 'x', 'info', 'target', 'score', 'feedback' + assert all('module' in r and 'x' in r and 'info' in r and 'target' in r and 'score' in r and 'feedback' in r for r in rollouts), \ + "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." - # # Convert all ParameterNode to data in the rollouts - # _rollouts = [] - # for r in rollouts: - # _r = {} - # for k, v in r.items(): - # if isinstance(v, trace.ParameterNode): - # _r[k] = v.data - # else: - # _r[k] = v - - # _rollouts.append(_r) # convert all ParameterNode to data self.rollouts.extend(rollouts) - # # XXX TODO hacky - # self.rollouts.rollouts.extend(_rollouts) # extend the rollouts with the def score(self): """ Compute the score of the candidate based on the rollouts. """ @@ -407,14 +375,9 @@ def score(self): return np.mean(scores) if scores else None -class PrioritySearch(SearchAlgorithm): +class PrioritySearch(SearchTemplate): + """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. """ - # def train(self, *args, - # num_candidates: int = 10, # number of candidates to propose - # default_score: Union[float, None] = None, # default score for the candidates - # validate_proposals: bool = True, # whether to validate the proposed parameters # TODO better naming - # **kwargs - # ): def train(self, guide, # guide to provide feedback train_dataset, # dataset of (x, info) pairs to train the agent @@ -424,7 +387,7 @@ def train(self, validate_guide = None, # to provide scores for the validation set # training loop batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size for broadcasting the agents + sub_batch_size = None, # sub-batch size that each optimizer attends to score_range = None, # minimum score to update the agent num_epochs = 1, # number of training epochs num_threads = None, # maximum number of threads to use @@ -439,19 +402,18 @@ def train(self, save_path: str = "checkpoints/agent.pkl", # path to save the agent # Priority Search specific parameters num_candidates: int = 10, # number of candidates to propose - default_score: Union[float, None] = None, # default score for the candidates + default_score: float = float('inf'), # default score assigned to priority queue candidates validate_proposals: bool = True, # whether to validate the proposed parameters # Additional keyword arguments **kwargs ): - # Create agents and optimizers for search - self.num_candidates = num_candidates # number of candidates to propose - self.score_range = score_range or (0., 1.) # XXX hacky now - self.default_score = default_score if default_score is not None else self.score_range[0] # default score for the candidates + self.num_candidates = num_candidates # number of candidates to propose by each optimizer call self.validate_proposals = validate_proposals # whether to validate the proposed parameters - self._queue = [(self.default_score, ModuleCandidate(self.agent))] # priority queue of ModuleCandidates, initialized with the base agent + self.default_score = default_score + self.memory = [(self.default_score, ModuleCandidate(self.agent))] # Priority queue of ModuleCandidates, initialized with the base agent + self._exploration_candidates = None super().train(guide, train_dataset, validate_dataset=validate_dataset, @@ -480,18 +442,21 @@ def update(self, samples=None, verbose=False, **kwargs): # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information # 4. Explore and exploit the priority queue - best_candidate = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - exploration_candidates = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + self._exploration_candidates = exploration_candidates - # TBD Log information about the update + # TODO Log information about the update info_log = { 'best_candidate_score': best_candidate.score(), 'num_exploration_candidates': len(exploration_candidates), } + info_log.update(info_exploit) # add the info from the exploit step + info_log.update(info_explore) # add the info from the explore step return best_candidate.update_dict, [c.get_module() for c in exploration_candidates], info_log - def propose(self, samples=None, verbose=False, n_proposals=1, **kwargs): + def propose(self, samples, verbose=False, n_proposals=1, **kwargs): """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. Args: @@ -503,18 +468,14 @@ def propose(self, samples=None, verbose=False, n_proposals=1, **kwargs): Returns: candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. """ - if samples is None: - parameters = self.optimizer.parameters # use the current parameters of the optimizer - update_dict = {p: p.data for p in parameters} # return the current parameters as the update dict - # TODO what to do here? should we return n_proposals variations? - return [update_dict] # return the update dict as a list assert isinstance(samples, Samples), "samples must be an instance of Samples." - samples = samples.samples + samples = samples.samples # list of RolloutsGraph objects + def _step(n, verbose=False, num_threads=None, **kwargs): """ Standard optimizer step for a single agent. """ # optimizer = self._optimizers[n] # get the optimizer for the n-th agent - # TODO this seems slow + # NOTE this seems slow optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one rollouts = samples[n] # RolloutsGraph @@ -549,68 +510,70 @@ def _step(n, verbose=False, num_threads=None, **kwargs): candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts] return candidates - - def validate(self, candidates, samples=None, verbose=False, **kwargs): + def validate(self, candidates, samples, verbose=False, **kwargs): """ Validate the proposed candidate parameters Args: - candidates (list of dict): A list of ModuleCandidate objects representing the proposed parameters. + candidates (list of ModuleCandidate): A list of ModuleCandidate objects representing the proposed parameters. samples (list of dict, optional): A list of samples collected in the current iteration. Defaults to None. verbose (bool, optional): Whether to print verbose output. Defaults to False. **kwargs: Additional keyword arguments that may be used by the implementation. Returns: - results (dict [ModuleCandidate, list of dict]): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. + results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. """ # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. if self._validate_dataset is None: # If no validation dataset is provided, use the current batch validate_dataset = samples.get_batch() # get the batch of inputs and infos from the samples - self.validate_sampler.loader.dataset = validate_dataset # set the validation dataset in the sampler + self.validate_sampler.dataset = validate_dataset # set the validation dataset in the sampler self.validate_sampler.batch_size = len(validate_dataset['inputs']) # set the batch size to the number of inputs in the validation dataset candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates validate_samples = Samples(*self.validate_sampler.sample(candidate_agents)) # list of RolloutsGraph objects - # TODO log _ + + exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration + assert exploration_candidates is not None, "exploration_candidates must be set before calling validate." if self.validate_proposals: if self._validate_dataset is None: + # NOTE this might contain some duplicates due to sub_batch_size < batch_size validate_samples.add_samples(samples) # if no validation dataset is provided, append the samples to the validate_samples else: # validate the agents in the validate_dataset - # TODO need a flag? - exploration_agents = [rollouts.module for rollouts in samples.samples] + # exploration_agents = [rollouts.module for rollouts in samples.samples] # NOTE this might contain some duplicates due to sub_batch_size < batch_size + exploitation_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents)) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples # Return a dict, key: ModuleCandidate, value: rollouts (list of dicts) - results = {} + # In validate_samples, there may be multiple rollouts collected by the same agent (or their copies). + # We need to group the rollouts by the agent (ModuleCandidate) and return a dictionary where the keys are the ModuleCandidate objects and the values are lists of rollouts (list of dicts). + results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) + for c in candidates + exploration_candidates: + # Initialize the candidate in the results dictionary + results[id(c)] = (c, []) # (ModuleCandidate, list of rollouts) + for rollouts in validate_samples.samples: - # rollouts is subgraph - agent = rollouts.module - index = candidate_agents.index(agent) - candidate = candidates[index] # get the candidate corresponding to the agent - # TODO delete 'module' from the rollouts dict? - if candidate in results: - # If the candidate already exists in results, we can append the rollouts to the existing list - results[candidate].extend(rollouts) - else: - # If the candidate does not exist in results, we create a new entry - results[candidate] = rollouts + module = rollouts.module # trace.Module + key = module._ModuleCandidate_candidate_id # use the candidate id as the key + if key not in results: + raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") + # Append the rollouts to the list of rollouts for the key + results[key][1].extend(rollouts.to_list()) return results def update_memory(self, validate_results, **kwargs): - """ Update the priority queue with the validation results. Args: validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. **kwargs: Additional keyword arguments that may be used by the implementation. """ - for candidate, rollouts in validate_results.items(): - candidate.add_rollouts(rollouts.to_list()) # add the rollouts to the candidate + for candidate_id, (candidate, rollouts) in validate_results.items(): + candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate - heapq.heappush(self._queue, (-score, candidate)) # add the candidate to the priority queue + heapq.heappush(self.memory, (-score, candidate)) # add the candidate to the priority queue #### @@ -624,13 +587,14 @@ def explore(self, **kwargs): """ # pop top self.num_candidates candidates from the priority queue top_candidates = [] - while len(top_candidates) < self.num_candidates and self._queue: - score, candidate = heapq.heappop(self._queue) + while len(top_candidates) < self.num_candidates and self.memory: + score, candidate = heapq.heappop(self.memory) top_candidates.append(candidate) # add the candidate to the top candidates - return top_candidates + return top_candidates, {} def exploit(self, **kwargs): + # NOTE This function can be overridden by subclasses to compute a different score """ Exploit the best candidate from the priority queue. This method should not change the priority queue. Args: **kwargs: Additional keyword arguments that may be used by the implementation. @@ -639,14 +603,31 @@ def exploit(self, **kwargs): """ # Right now, we just return the best candidate from the priority queue # This function can be overridden by subclasses to implement a different exploitation strategy - if not self._queue: + if not self.memory: raise ValueError("The priority queue is empty. Cannot exploit.") - best = min(self._queue) # (score, candidate) - return best[1] + best = min(self.memory) # (score, candidate) + score, best_candidate = best + score = -score # remember that we stored negative scores in the priority queue + return best_candidate, { + 'best_candidate_score': score, # remember that we stored negative scores in the priority queue + } + + def compute_score(self, candidate): - # By default, we compute the mean score of the rollouts # NOTE This function can be overridden by subclasses to compute a different score + """ Compute the score for the candidate based on the rollouts during the validation phase. + It can be overridden by subclasses to implement a different scoring strategy. + + Args: + candidate (ModuleCandidate): The candidate for which to compute the score. + Returns: + float: The computed score for the candidate. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + # By default, we compute the mean score of the rollouts + scores = [r['score'] for r in candidate.rollouts] default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 568575d0..9e1037a2 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -212,6 +212,34 @@ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=No if forward is None: self.forward = standard_forward + @property + def dataset(self): + """ Get the dataset of the loader. """ + return self.loader.dataset + + @dataset.setter + def dataset(self, value): + """ Set the dataset of the loader. """ + assert isinstance(value, dict), "Dataset must be a dictionary with 'inputs' and 'infos' keys." + assert 'inputs' in value and 'infos' in value, "Dataset must contain 'inputs' and 'infos' keys." + assert len(value['inputs']) == len(value['infos']), "Length of inputs must match length of infos." + self.loader.dataset = value + + @property + def batch_size(self): + """ Get the batch size of the loader. """ + return self.loader.batch_size + + @batch_size.setter + def batch_size(self, value): + """ Set the batch size of the loader. """ + self.loader.batch_size = value + + @property + def n_epochs(self): + """ Get the number of epochs of the loader. """ + return self.loader.n_epochs + def sample(self, agents): """ Sample a batch of data from the loader and evaluate the agents. From abcf7dd94f56c7021f6c3fdb054e8bddb25b7c66 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 15 Jul 2025 03:52:43 +0000 Subject: [PATCH 117/172] Add unit test for sampler. --- tests/unit_tests/test_sampler.py | 133 +++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tests/unit_tests/test_sampler.py diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py new file mode 100644 index 00000000..0ac4d104 --- /dev/null +++ b/tests/unit_tests/test_sampler.py @@ -0,0 +1,133 @@ +from opto import trace +from opto.trainer.sampler import Sampler +from opto.trainer.loader import DataLoader +from opto.trainer.guide import AutoGuide +from opto.trainer.algorithms.search_algorithms import is_node_copy + + +class Guide(AutoGuide): + + def get_feedback(self, query, response, reference=None, **kwargs): + """ + Provide feedback based on the query and response. + + Args: + query: The query to analyze. + response: The response generated by the model. + reference: Optional reference answer for comparison. + **kwargs: Additional context or parameters. + + Returns: + A tuple containing a score and feedback string. + """ + score = response == reference + feedback = "Exact match!" if score == 1.0 else "Not an exact match." + return score, feedback + +@trace.model +class Agent: + + def __init__(self): + self.param = trace.node(1., trainable=True) + self.state = 0 + + @trace.bundle() + def forward(self, x): + self.state += 1 + return self.state + + +def test_sample_with_single_agent(): + + xs = [1, 2, 3, 4, 5] + infos = [1, 2, 3, 4, 5] + batch_size = 3 + sub_batch_size = 2 + num_threads = 2 + dataset = {'inputs': xs, 'infos': infos} + loader = DataLoader(dataset, batch_size=batch_size, randomize=False) + sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) + + + ## Test with a single agent + samples, batch = sampler.sample([Agent()]) + + # check batch is equal to dataset's first batch_size elements + assert batch['inputs'] == dataset['inputs'][:3] + assert batch['infos'] == dataset['infos'][:3] + + assert len(samples) == 2 + + # a batch of 3 is split into 2 sub-batches of size 2 and 1 + assert is_node_copy(samples[0].module.parameters()[0], samples[1].module.parameters()[0]) + assert len(samples[0].rollouts) == 2 + assert len(samples[1].rollouts) == 1 + + for rollouts in samples: + for rollout in rollouts: + assert rollout.target == 1 # state is not affected by multiple calls + + + samples, batch = sampler.sample([Agent()]) + + # check batch is equal to dataset's second batch_size elements + assert batch['inputs'] == dataset['inputs'][3:] + assert batch['infos'] == dataset['infos'][3:] + assert len(samples) == 1 + assert len(samples[0].rollouts) == 2 + + for rollouts in samples: + for rollout in rollouts: + assert rollout.target == 1 # state is not affected by multiple calls + + +def test_sample_with_multiple_agents(): + """ + Test sampling with multiple agents. + This will create a batch of samples from two agents. + """ + + xs = [1, 2, 3, 4, 5] + infos = [1, 2, 3, 4, 5] + batch_size = 3 + sub_batch_size = 2 + num_threads = 2 + dataset = {'inputs': xs, 'infos': infos} + loader = DataLoader(dataset, batch_size=batch_size, randomize=False) + sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) + + + ## Test with multiple agents + samples, batch = sampler.sample([Agent(), Agent()]) + + # check batch is equal to dataset's first batch_size elements + assert batch['inputs'] == dataset['inputs'][:3] + assert batch['infos'] == dataset['infos'][:3] + + assert len(samples) == 4, f"Expected 4 samples, got {len(samples)}" + + assert is_node_copy(samples[0].module.parameters()[0], samples[1].module.parameters()[0]) + assert len(samples[0].rollouts) == 2 + assert len(samples[1].rollouts) == 1 + + assert is_node_copy(samples[2].module.parameters()[0], samples[3].module.parameters()[0]) + assert len(samples[2].rollouts) == 2 + assert len(samples[3].rollouts) == 1 + + for rollouts in samples: + for rollout in rollouts: + assert rollout.target == 1 # state is not affected by multiple calls + + samples, batch = sampler.sample([Agent(), Agent()]) + # check batch is equal to dataset's second batch_size elements + assert batch['inputs'] == dataset['inputs'][3:] + assert batch['infos'] == dataset['infos'][3:] + + assert len(samples) == 2, f"Expected 2 samples, got {len(samples)}" + + assert len(samples[0].rollouts) == 2 + assert len(samples[1].rollouts) == 2 + + for rollouts in samples: + for rollout in rollouts: + assert rollout.target == 1 # state is not affected by multiple calls \ No newline at end of file From 07bb4c7649e690f8af1bdf43413d974aba08db36 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Tue, 15 Jul 2025 22:28:49 -0500 Subject: [PATCH 118/172] fixed a bug about remapped_update_dict --- opto/trainer/algorithms/search_algorithms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 2f23f9c9..85c3bd57 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -82,8 +82,8 @@ def set_module_parameters(agent, update_dict): The update_dict is a dictionary of ParameterNode: value pairs. The agent's parameters will be updated with the values from the update_dict. """ - remap_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters - for k, v in remap_update_dict.items(): + remapped_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters + for k, v in remapped_update_dict.items(): k._data = v # set the parameter's data to the value in the update_dict def create_module_from_update_dict(agent, update_dict): From 166c9781fac815a979c38f9d9bb1410b073bb2c7 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Tue, 15 Jul 2025 22:32:07 -0500 Subject: [PATCH 119/172] add ucb reward --- examples/gsm8k_search_algo.py | 15 +++---- opto/trainer/algorithms/search_algorithms.py | 45 ++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py index 832ec8e1..82177401 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/gsm8k_search_algo.py @@ -3,8 +3,8 @@ from opto import trace from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrime -from opto.trainer.algorithms.search_algorithms import PrioritySearch as SearchAlgorithm -from opto.trainer.loggers import TensorboardLogger +from opto.trainer.algorithms.search_algorithms import UCBSearch as SearchAlgorithm +from opto.trainer.loggers import WandbLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @@ -47,8 +47,7 @@ def forward(self, message: Any) -> Any: Guide = VerbalJudgeGuide -Logger = TensorboardLogger - +Logger = WandbLogger def main(): # set seed @@ -58,9 +57,9 @@ def main(): eval_frequency = -1 num_threads = 3 verbose = True - teacher_model = None # use default model - student_model = None # use default model - optimizer_model = None # use default model + teacher_model = "vertex_ai/gemini-2.0-flash" # use default model + student_model = "vertex_ai/gemini-2.0-flash" # use default model + optimizer_model = "vertex_ai/gemini-2.0-flash" # use default model np.random.seed(seed) @@ -73,7 +72,7 @@ def main(): agent = Learner(llm=LLM(student_model)) guide = Guide(llm=LLM(teacher_model)) optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) - logger = Logger(verbose=verbose) + logger = Logger(project="gsm8k-examples", name="ucb",verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format alg = SearchAlgorithm( diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 85c3bd57..bbf551bc 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -632,3 +632,48 @@ def compute_score(self, candidate): default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates return np.mean(scores) if scores else self.default_score + +class UCBSearch(PrioritySearch): + """A search algorithm that keeps a buffer with candidates and their UCB scores. It does exploration according to the UCB score.""" + + def __init__(self, *args, exploration_constant=1.0, **kwargs): + """Initialize UCBSearch with an exploration constant for the UCB formula.""" + super().__init__(*args, **kwargs) + self.exploration_constant = exploration_constant + + def compute_score(self, candidate): + """Compute the UCB score for the candidate. + + UCB = mean_score + exploration_constant * sqrt(ln(total_trials) / candidate_trials) + + Args: + candidate (ModuleCandidate): The candidate for which to compute the UCB score. + Returns: + float: The computed UCB score for the candidate. + """ + if not isinstance(candidate, ModuleCandidate): + raise TypeError("candidate must be an instance of ModuleCandidate.") + + # Get scores from rollouts + scores = [r['score'] for r in candidate.rollouts] + + # If no rollouts, return a high exploration score to encourage trying this candidate + if not scores: + return float('inf') # Maximum exploration for untried candidates + + # Calculate mean score for this candidate + mean_score = np.mean(scores) + candidate_trials = len(scores) + + # Calculate total trials across all candidates in memory + total_trials = sum(len(c.rollouts) for _, c in self.memory) + + # Handle edge case where total_trials is 0 or 1 + if total_trials <= 1: + return mean_score + + # Calculate UCB score + exploration_term = self.exploration_constant * np.sqrt(np.log(total_trials) / candidate_trials) + ucb_score = mean_score + exploration_term + + return ucb_score From c360e9a27139a5dd959099702fab20cc54dd9925 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 16 Jul 2025 06:12:51 +0000 Subject: [PATCH 120/172] Fix some bugs in PrioritySearch. Update search_algo example script. --- examples/gsm8k_search_algo.py | 13 ++++++++++--- opto/trainer/algorithms/search_algorithms.py | 9 ++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py index 832ec8e1..1243cd8f 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/gsm8k_search_algo.py @@ -54,9 +54,13 @@ def main(): # set seed seed = 42 num_epochs = 1 - batch_size = 1 + batch_size = 3 + sub_batch_size = 2 + score_range = (0, 1) # range of the score for the guide eval_frequency = -1 - num_threads = 3 + num_eval_samples = 2 + num_threads = 10 + datasize = 5 verbose = True teacher_model = None # use default model student_model = None # use default model @@ -66,7 +70,7 @@ def main(): # In this example, we use the GSM8K dataset, which is a dataset of math word problems. # We will look the training error of the agent on a small portion of this dataset. - train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10] + train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:datasize] train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) test_dataset = train_dataset @@ -88,6 +92,9 @@ def main(): eval_frequency=eval_frequency, test_dataset=test_dataset, num_threads=num_threads, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_eval_samples=num_eval_samples, verbose='output' if verbose else False) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 85c3bd57..fd8eb1bc 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -261,8 +261,10 @@ def sample(self, agents, verbose=False, **kwargs): samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch # Log information about the sampling + scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph + scores = [item for sublist in scores for item in sublist] # flatten the list of scores log_info = { - 'mean_score': np.mean([ g.get_scores() for g in samples.samples]), + 'mean_score': np.mean(scores), 'n_epochs': self.train_sampler.n_epochs, } return samples, log_info @@ -328,7 +330,7 @@ def __init__(self, def get_module(self): """ Apply the update_dict to the base_module and return the updated module. This will not update the base_module itself.""" - module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else self.base_module + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) module._ModuleCandidate_candidate_id = id(self) # set the id of the module to the id of the candidate; this is used to identify the candidate in the priority queue return module # return the updated module @@ -545,7 +547,8 @@ def validate(self, candidates, samples, verbose=False, **kwargs): validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples - # Return a dict, key: ModuleCandidate, value: rollouts (list of dicts) + # TODO some ModuleCandidate are the same in parameters though they have different ids + # In validate_samples, there may be multiple rollouts collected by the same agent (or their copies). # We need to group the rollouts by the agent (ModuleCandidate) and return a dictionary where the keys are the ModuleCandidate objects and the values are lists of rollouts (list of dicts). results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) From 58b0c66d75b76434bf9503ff03d7b415a0a41191 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 11:36:28 -0400 Subject: [PATCH 121/172] add JSON option through OptimizerPromptSymbolSetJSON --- opto/optimizers/optoprime_v2.py | 237 +++++++++++++++++++++++--------- 1 file changed, 170 insertions(+), 67 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 62a1c0e0..bbdeaba4 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -127,7 +127,6 @@ def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", return result - class OptimizerPromptSymbolSet: """ By inheriting this class and pass into the optimizer. People can change the optimizer documentation @@ -138,6 +137,8 @@ class OptimizerPromptSymbolSet: - Output format: the format of the output of the optimizer """ + # Titles should be written as markdown titles (space between # and title) + # In text, we automatically remove space in the title, so it will become `#Title` variables_section_title = "# Variables" inputs_section_title = "# Inputs" outputs_section_title = "# Outputs" @@ -158,22 +159,85 @@ class OptimizerPromptSymbolSet: improved_variable_tag = "variable" name_tag = "name" - # custom output format (this will give the highest degree of freedom) - # once it's set, it will override the default output format - output_format_prompt_instruction = None + # custom output format + # if this is not None, then the user needs to implement the following functions: + # - output_response_extractor + # - example_output + custom_output_format_instruction = None + + @property + def output_format(self) -> str: + """ + This function defines the input to: + ``` + {output_format} + ``` + In the self.output_format_prompt_template in the OptoPrimeV2 + """ + if self.custom_output_format_instruction is None: + # we use a default XML like format + return dedent(f""" + <{self.reasoning_tag}> + reasoning + + <{self.improved_variable_tag}> + <{self.name_tag}>variable_name + <{self.value_tag}> + value + + + """) + else: + return self.custom_output_format_instruction.strip() + + def example_output(self, reasoning, variables): + """ + reasoning: str + variables: format {variable_name, value} + """ + if self.custom_output_format_instruction is not None: + raise NotImplementedError + else: + # Build the output string in the same XML-like format as self.output_format + output = [] + output.append(f"<{self.reasoning_tag}>") + output.append(reasoning) + output.append(f"") + for var_name, value in variables.items(): + output.append(f"<{self.improved_variable_tag}>") + output.append(f"<{self.name_tag}>{var_name}") + output.append(f"<{self.value_tag}>") + output.append(str(value)) + output.append(f"") + output.append(f"") + return "\n".join(output) + def output_response_extractor(self, response: str) -> Dict[str, Any]: - if self.output_format_prompt_instruction is None: + # the response here should just be plain text + + if self.custom_output_format_instruction is None: extracted_data = extract_xml_like_data(response, reasoning_tag=self.reasoning_tag, improved_variable_tag=self.improved_variable_tag, name_tag=self.name_tag, value_tag=self.value_tag) + + # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) + # then we remove such suggestion + keys_to_remove = [] + for key, value in extracted_data['variables'].items(): + if "__code" in key and value.strip() == "": + keys_to_remove.append(key) + + for key in keys_to_remove: + del extracted_data['variables'][key] + return extracted_data else: raise NotImplementedError( "If you supplied a custom output format prompt template, you need to implement your own response extractor") - + @property def default_prompt_symbols(self) -> Dict[str, str]: return { @@ -187,6 +251,91 @@ def default_prompt_symbols(self) -> Dict[str, str]: "documentation": self.documentation_section_title, } +class OptimizerPromptSymbolSetJSON(OptimizerPromptSymbolSet): + """We enforce a JSON output format extraction""" + + custom_output_format_instruction = """ + {{ + "reasoning": , + "suggestion": {{ + : , + : , + }} + }} + """ + + def example_output(self, reasoning, variables): + """ + reasoning: str + variables: format {variable_name, value} + """ + + # Build the output string in the same JSON format as described in custom_output_format_instruction + output = { + "reasoning": reasoning, + "suggestion": {var_name: value for var_name, value in variables.items()} + } + return json.dumps(output, indent=2) + + def output_response_extractor(self, response: str) -> Dict[str, Any]: + reasoning = "" + suggestion_tag = "suggestion" + + if "```" in response: + response = response.replace("```", "").strip() + + suggestion = {} + attempt_n = 0 + while attempt_n < 2: + try: + suggestion = json.loads(response)[suggestion_tag] + reasoning = json.loads(response)[self.reasoning_tag] + break + except json.JSONDecodeError: + # Remove things outside the brackets + response = re.findall(r"{.*}", response, re.DOTALL) + if len(response) > 0: + response = response[0] + attempt_n += 1 + except Exception: + attempt_n += 1 + + if not isinstance(suggestion, dict): + suggestion = {} + + if len(suggestion) == 0: + # we try to extract key/value separately and return it as a dictionary + pattern = rf'"{suggestion_tag}"\s*:\s*\{{(.*?)\}}' + suggestion_match = re.search(pattern, str(response), re.DOTALL) + if suggestion_match: + suggestion = {} + # Extract the entire content of the suggestion dictionary + suggestion_content = suggestion_match.group(1) + # Regex to extract each key-value pair; + # This scheme assumes double quotes but is robust to missing commas at the end of the line + pair_pattern = r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"' + # Find all matches of key-value pairs + pairs = re.findall(pair_pattern, suggestion_content, re.DOTALL) + for key, value in pairs: + suggestion[key] = value + + if len(suggestion) == 0: + print(f"Cannot extract suggestion from LLM's response:") + print(response) + + # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) + # then we remove such suggestion + keys_to_remove = [] + for key, value in suggestion.items(): + if "__code" in key and value.strip() == "": + keys_to_remove.append(key) + for key in keys_to_remove: + del suggestion[key] + + extracted_data = {"reasoning": reasoning, + "variables": suggestion} + + return extracted_data class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): variables_section_title = "# Variables" @@ -207,7 +356,6 @@ class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): reasoning_tag = "reason" improved_variable_tag = "var" name_tag = "name" - value_tag = "data" @dataclass @@ -262,7 +410,7 @@ def __repr__(self) -> str: others=self.others, feedback=self.feedback, ), self.optimizer_prompt_symbol_set.default_prompt_symbols) - + def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: default_prompt_symbols = { "variables": "# Variables", @@ -275,27 +423,13 @@ def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: "code": "# Code", "documentation": "# Documentation", } - + for k, v in symbols.items(): text = text.replace(default_prompt_symbols[k], v) return text -# TODO: solution1 -> solution2 -> solution3 -# TODO: param(solution) optimzer.step(solution, "reward is 1, maximize1) -> solution 2 -# TODO: maybe have a trace.train() # simpler even than Algorithm, and cover 80% of use cases - class OptoPrimeV2(OptoPrime): - # TODO: LLM has the option to check the value of truncated one - # TODO: turn into a conversation round - # TODO: and show in a separate message - # TODO: 3. Compact representation (compress function) - # TODO: batchify, list of inputs, output is a list of inputs - # TODO: information is redundant - # TODO: idea 1: for each operator, we can identify repeated structure - # TODO: idea 2: for each bundle/op, the user can pass in a callable function, take original output, return a string - # TODO: idea 2-2: each node has a string representation of data, that's what the optimizer should use (this string is fixed) - # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( """ @@ -424,19 +558,11 @@ def __init__( self.example_problem_summary.inputs = {'b': (1, None), 'c': (5, None)} self.example_problem = self.problem_instance(self.example_problem_summary) - self.example_response = dedent( - f""" - <{self.optimizer_prompt_symbol_set.reasoning_tag}> - In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10. - - - <{self.optimizer_prompt_symbol_set.improved_variable_tag}> - <{self.optimizer_prompt_symbol_set.name_tag}>a - <{self.optimizer_prompt_symbol_set.value_tag}> - 10 - - - """ + self.example_response = self.optimizer_prompt_symbol_set.example_output( + reasoning="In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.", + variables={ + 'a': 10, + } ) self.include_example = include_example @@ -473,17 +599,7 @@ def initialize_prompt(self): others_section_title=self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") ) self.output_format_prompt = self.output_format_prompt_template.format( - output_format=dedent(f""" - <{self.optimizer_prompt_symbol_set.reasoning_tag}> - reasoning - - <{self.optimizer_prompt_symbol_set.improved_variable_tag}> - <{self.optimizer_prompt_symbol_set.name_tag}>variable_name - <{self.optimizer_prompt_symbol_set.value_tag}> - value - - - """), + output_format=self.optimizer_prompt_symbol_set.output_format, reasoning_tag=self.optimizer_prompt_symbol_set.reasoning_tag, improved_variable_tag=self.optimizer_prompt_symbol_set.improved_variable_tag, instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), @@ -595,37 +711,37 @@ def problem_instance(self, summary, mask=None): instruction=self.objective if "#Instruction" not in mask else "", code=( "\n".join([v for k, v in sorted(summary.graph)]) - if "#Code" not in mask + if self.optimizer_prompt_symbol_set.inputs_section_title not in mask else "" ), documentation=( "\n".join([f"[{k}] {v}" for k, v in summary.documentation.items()]) - if "#Documentation" not in mask + if self.optimizer_prompt_symbol_set.documentation_section_title not in mask else "" ), variables=( self.repr_node_value_compact(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) - if "#Variables" not in mask + if self.optimizer_prompt_symbol_set.variables_section_title not in mask else "" ), inputs=( self.repr_node_value_compact(summary.inputs, node_tag=self.optimizer_prompt_symbol_set.node_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, - constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Inputs" not in mask else "" + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.inputs_section_title not in mask else "" ), outputs=( self.repr_node_value_compact(summary.output, node_tag=self.optimizer_prompt_symbol_set.node_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, - constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Outputs" not in mask else "" + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.outputs_section_title not in mask else "" ), others=( self.repr_node_value_compact(summary.others, node_tag=self.optimizer_prompt_symbol_set.node_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, - constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if "#Others" not in mask else "" + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.others_section_title not in mask else "" ), - feedback=summary.user_feedback if "#Feedback" not in mask else "", + feedback=summary.user_feedback if self.optimizer_prompt_symbol_set.feedback_section_title not in mask else "", optimizer_prompt_symbol_set=self.optimizer_prompt_symbol_set ) @@ -636,9 +752,6 @@ def _step( summary = self.summarize() system_prompt, user_prompt = self.construct_prompt(summary, mask=mask) - system_prompt = self.replace_symbols(system_prompt, self.prompt_symbols) - user_prompt = self.replace_symbols(user_prompt, self.prompt_symbols) - response = self.call_llm( system_prompt=system_prompt, user_prompt=user_prompt, @@ -670,7 +783,6 @@ def _step( def extract_llm_suggestion(self, response: str): """Extract the suggestion from the response.""" - # suggestion = extract_xml_like_data(response) suggestion = self.optimizer_prompt_symbol_set.output_response_extractor(response) if len(suggestion) == 0: @@ -678,15 +790,6 @@ def extract_llm_suggestion(self, response: str): print("Cannot extract suggestion from LLM's response:") print(response) - # if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present) - # then we remove such suggestion - keys_to_remove = [] - for key, value in suggestion.items(): - if "__code" in key and value.strip() == "": - keys_to_remove.append(key) - for key in keys_to_remove: - del suggestion[key] - return suggestion def call_llm( From 0bb8d44c21550ad6ff9fa36563ac1cc25274f508 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 11:44:22 -0400 Subject: [PATCH 122/172] add enforce_json flag --- opto/optimizers/optoprime_v2.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index bbdeaba4..e4becad2 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -159,6 +159,8 @@ class OptimizerPromptSymbolSet: improved_variable_tag = "variable" name_tag = "name" + expect_json = False # this will stop `enforce_json` arguments passed to LLM calls + # custom output format # if this is not None, then the user needs to implement the following functions: # - output_response_extractor @@ -254,6 +256,8 @@ def default_prompt_symbols(self) -> Dict[str, str]: class OptimizerPromptSymbolSetJSON(OptimizerPromptSymbolSet): """We enforce a JSON output format extraction""" + expect_json = True + custom_output_format_instruction = """ {{ "reasoning": , @@ -533,9 +537,11 @@ def __init__( log=True, initial_var_char_limit=100, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OptimizerPromptSymbolSet(), + use_json_object_format=True, # whether to use json object format for the response when calling LLM **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) + self.use_json_object_format = use_json_object_format if optimizer_prompt_symbol_set.expect_json and use_json_object_format else False self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() self.objective = objective or self.default_objective.format(value_tag=optimizer_prompt_symbol_set.value_tag, @@ -808,7 +814,9 @@ def call_llm( {"role": "user", "content": user_prompt}, ] - response = self.llm(messages=messages, max_tokens=max_tokens) + response_format = {"type": "json_object"} if self.use_json_object_format else None + + response = self.llm(messages=messages, max_tokens=max_tokens, response_format=response_format) response = response.choices[0].message.content From a5d5ee9b88e935c94403dada42bc919238868b25 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 11:51:55 -0400 Subject: [PATCH 123/172] moved truncate_expressions outside --- opto/optimizers/optoprime_v2.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index e4becad2..d9bbca5b 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -432,6 +432,12 @@ def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: text = text.replace(default_prompt_symbols[k], v) return text +def truncate_expression(value, limit): + # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr + value = str(value) + if len(value) > limit: + return value[:limit] + "...(skipped due to length limit)" + return value class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. @@ -538,9 +544,13 @@ def __init__( initial_var_char_limit=100, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OptimizerPromptSymbolSet(), use_json_object_format=True, # whether to use json object format for the response when calling LLM + truncate_expression=truncate_expression, **kwargs, ): super().__init__(parameters, *args, propagator=propagator, **kwargs) + + self.truncate_expression = truncate_expression + self.use_json_object_format = use_json_object_format if optimizer_prompt_symbol_set.expect_json and use_json_object_format else False self.ignore_extraction_error = ignore_extraction_error self.llm = llm or LLM() @@ -655,13 +665,6 @@ def repr_node_value_compact(self, node_dict, node_tag="node", f"<{node_tag} name=\"{k}\" type=\"code\">\n<{value_tag}>\n{signature}{node_value}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) - def truncate_expression(self, value, limit): - # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr - value = str(value) - if len(value) > limit: - return value[:limit] + "...(skipped due to length limit)" - return value - def construct_prompt(self, summary, mask=None, *args, **kwargs): """Construct the system and user prompt.""" system_prompt = ( From b2766ace3000388a51dfefa4f69f991d6931995e Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 11:57:39 -0400 Subject: [PATCH 124/172] moved helper function to utils --- opto/optimizers/optoprime_v2.py | 118 +------------------------------ opto/optimizers/utils.py | 119 ++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 117 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index d9bbca5b..db651bfb 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, asdict from opto.optimizers.optoprime import OptoPrime, FunctionFeedback from opto.trace.utils import dedent +from opto.optimizers.utils import truncate_expression, extract_xml_like_data from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator @@ -16,117 +17,6 @@ from typing import Dict, Any -def extract_top_level_blocks(text: str, tag: str): - """Extract all top-level ... blocks from text.""" - blocks = [] - start_tag = f'<{tag}>' - end_tag = f'' - stack = [] - start = None - i = 0 - while i < len(text): - if text.startswith(start_tag, i): - if not stack: - start = i + len(start_tag) - stack.append(i) - i += len(start_tag) - elif text.startswith(end_tag, i): - if stack: - stack.pop() - if not stack and start is not None: - blocks.append(text[start:i]) - start = None - i += len(end_tag) - else: - i += 1 - return blocks - - -def extract_first_top_level_block(text: str, tag: str): - blocks = extract_top_level_blocks(text, tag) - return blocks[0] if blocks else None - - -def strip_nested_blocks(text: str, tag: str) -> str: - """Remove all nested ... blocks from text, leaving only the top-level text.""" - result = '' - start_tag = f'<{tag}>' - end_tag = f'' - stack = [] - i = 0 - last = 0 - while i < len(text): - if text.startswith(start_tag, i): - if not stack: - result += text[last:i] - stack.append(i) - i += len(start_tag) - elif text.startswith(end_tag, i): - if stack: - stack.pop() - if not stack: - last = i + len(end_tag) - i += len(end_tag) - else: - i += 1 - if not stack: - result += text[last:] - return result.strip() - - -def extract_reasoning_and_remainder(text: str, tag: str = "reasoning"): - """Extract reasoning and the remainder of the text after reasoning block (if closed). Strip whitespace only if properly closed.""" - start_tag = f'<{tag}>' - end_tag = f'' - start = text.find(start_tag) - if start == -1: - return '', text - start += len(start_tag) - end = text.find(end_tag, start) - if end == -1: - # If not properly closed, don't strip whitespace to preserve original formatting - return text[start:], '' - return text[start:end].strip(), text[end + len(end_tag):] - - -def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", - improved_variable_tag: str = "variable", - name_tag: str = "name", - value_tag: str = "value") -> Dict[str, Any]: - """ - Extract thinking content and improved variables from text containing XML-like tags. - - Args: - text (str): Text containing and tags - - Returns: - Dict containing: - - 'reasoning': content of element - - 'variables': dict mapping variable names to their values - """ - result = { - 'reasoning': '', - 'variables': {} - } - - # Extract reasoning and the remainder of the text - reasoning, remainder = extract_reasoning_and_remainder(text, reasoning_tag) - result['reasoning'] = reasoning - - # Only parse variables from the remainder (i.e., after a closed reasoning tag) - variable_blocks = extract_top_level_blocks(remainder, improved_variable_tag) - for var_block in variable_blocks: - name_block = extract_first_top_level_block(var_block, name_tag) - value_block = extract_first_top_level_block(var_block, value_tag) - # Only add if both name and value tags are present and name is non-empty after stripping - if name_block is not None and value_block is not None: - var_name = name_block.strip() - var_value = value_block.strip() if value_block is not None else '' - if var_name: # Only require name to be non-empty, value can be empty - result['variables'][var_name] = var_value - return result - - class OptimizerPromptSymbolSet: """ By inheriting this class and pass into the optimizer. People can change the optimizer documentation @@ -432,12 +322,6 @@ def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: text = text.replace(default_prompt_symbols[k], v) return text -def truncate_expression(value, limit): - # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr - value = str(value) - if len(value) > limit: - return value[:limit] + "...(skipped due to length limit)" - return value class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. diff --git a/opto/optimizers/utils.py b/opto/optimizers/utils.py index 2f076f54..711b81aa 100644 --- a/opto/optimizers/utils.py +++ b/opto/optimizers/utils.py @@ -13,3 +13,122 @@ def print_color(message, color=None, logger=None): if logger is not None: logger.log(message) + + +def truncate_expression(value, limit): + # https://stackoverflow.com/questions/1436703/what-is-the-difference-between-str-and-repr + value = str(value) + if len(value) > limit: + return value[:limit] + "...(skipped due to length limit)" + return value + + +def extract_top_level_blocks(text: str, tag: str): + """Extract all top-level ... blocks from text.""" + blocks = [] + start_tag = f'<{tag}>' + end_tag = f'' + stack = [] + start = None + i = 0 + while i < len(text): + if text.startswith(start_tag, i): + if not stack: + start = i + len(start_tag) + stack.append(i) + i += len(start_tag) + elif text.startswith(end_tag, i): + if stack: + stack.pop() + if not stack and start is not None: + blocks.append(text[start:i]) + start = None + i += len(end_tag) + else: + i += 1 + return blocks + + +def extract_first_top_level_block(text: str, tag: str): + blocks = extract_top_level_blocks(text, tag) + return blocks[0] if blocks else None + + +def strip_nested_blocks(text: str, tag: str) -> str: + """Remove all nested ... blocks from text, leaving only the top-level text.""" + result = '' + start_tag = f'<{tag}>' + end_tag = f'' + stack = [] + i = 0 + last = 0 + while i < len(text): + if text.startswith(start_tag, i): + if not stack: + result += text[last:i] + stack.append(i) + i += len(start_tag) + elif text.startswith(end_tag, i): + if stack: + stack.pop() + if not stack: + last = i + len(end_tag) + i += len(end_tag) + else: + i += 1 + if not stack: + result += text[last:] + return result.strip() + + +def extract_reasoning_and_remainder(text: str, tag: str = "reasoning"): + """Extract reasoning and the remainder of the text after reasoning block (if closed). Strip whitespace only if properly closed.""" + start_tag = f'<{tag}>' + end_tag = f'' + start = text.find(start_tag) + if start == -1: + return '', text + start += len(start_tag) + end = text.find(end_tag, start) + if end == -1: + # If not properly closed, don't strip whitespace to preserve original formatting + return text[start:], '' + return text[start:end].strip(), text[end + len(end_tag):] + + +def extract_xml_like_data(text: str, reasoning_tag: str = "reasoning", + improved_variable_tag: str = "variable", + name_tag: str = "name", + value_tag: str = "value") -> Dict[str, Any]: + """ + Extract thinking content and improved variables from text containing XML-like tags. + + Args: + text (str): Text containing and tags + + Returns: + Dict containing: + - 'reasoning': content of element + - 'variables': dict mapping variable names to their values + """ + result = { + 'reasoning': '', + 'variables': {} + } + + # Extract reasoning and the remainder of the text + reasoning, remainder = extract_reasoning_and_remainder(text, reasoning_tag) + result['reasoning'] = reasoning + + # Only parse variables from the remainder (i.e., after a closed reasoning tag) + variable_blocks = extract_top_level_blocks(remainder, improved_variable_tag) + for var_block in variable_blocks: + name_block = extract_first_top_level_block(var_block, name_tag) + value_block = extract_first_top_level_block(var_block, value_tag) + # Only add if both name and value tags are present and name is non-empty after stripping + if name_block is not None and value_block is not None: + var_name = name_block.strip() + var_value = value_block.strip() if value_block is not None else '' + if var_name: # Only require name to be non-empty, value can be empty + result['variables'][var_name] = var_value + return result From b69f68f10efc5dc9abe6be93f96e85edcb2e3e9f Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 16 Jul 2025 12:22:16 -0400 Subject: [PATCH 125/172] fix typing import error --- opto/optimizers/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opto/optimizers/utils.py b/opto/optimizers/utils.py index 711b81aa..13a5ad01 100644 --- a/opto/optimizers/utils.py +++ b/opto/optimizers/utils.py @@ -1,3 +1,5 @@ +from typing import Dict, Any + def print_color(message, color=None, logger=None): colors = { "red": "\033[91m", From f9c78f342ef55255596b9698c22609c94b42f1fc Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 16 Jul 2025 21:37:54 +0000 Subject: [PATCH 126/172] Bring back ucb from experimental. --- opto/trainer/algorithms/UCBsearch.py | 374 +++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 opto/trainer/algorithms/UCBsearch.py diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py new file mode 100644 index 00000000..9ff6f61b --- /dev/null +++ b/opto/trainer/algorithms/UCBsearch.py @@ -0,0 +1,374 @@ +import numpy as np +import copy +import math +from collections import deque +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trainer.utils import async_run # Assuming print_color is in utils +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate, batchify # evaluate and batchify might be useful + +class UCBSearchAlgorithm(MinibatchAlgorithm): + """ + UCB Search Algorithm. + + Keeps a buffer of candidates with their statistics (score sum, evaluation count). + In each iteration: + 1. Picks a candidate 'a' from the buffer with the highest UCB score. + 2. Updates the optimizer with 'a's parameters. + 3. Draws a minibatch from the training set, performs a forward/backward pass, and calls optimizer.step() to get a new candidate 'a''. + 4. Evaluates 'a'' on a validation set minibatch. + 5. Updates statistics of 'a' (based on the training minibatch). + 6. Adds 'a'' (with its validation stats) to the buffer. + 7. If the buffer is full, evicts the candidate with the lowest UCB score. + """ + + def __init__(self, + agent: trace.Module, + optimizer, + max_buffer_size: int = 10, + ucb_exploration_factor: float = 1.0, # Controls exploration vs exploitation tradeoff in UCB selection + # UCB formula: μ(a) + c * sqrt(ln(t) / n(a)), c is the exploration factor + logger=None, + num_threads: int = None, + *args, + **kwargs): + super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) + + self.buffer = deque(maxlen=max_buffer_size) + self.max_buffer_size = max_buffer_size + # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, + # lower values favor exploitation of well-performing candidates. + self.ucb_exploration_factor = ucb_exploration_factor + + # To ensure optimizer_step can be called with bypassing=True if needed. + # This depends on the specific optimizer's implementation. + # For now, we assume the optimizer has a step method that can return parameters. + if not hasattr(self.optimizer, 'step'): + raise ValueError("Optimizer must have a 'step' method.") + + self._total_evaluations_tracker = 0 # Tracks total number of individual candidate evaluations used in UCB calculation for log(T) + self._candidate_id_counter = 0 + + def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> Tuple[List[Any], List[Any]]: + """Sample a minibatch from the dataset.""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos'): + print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') + return [], [] + + dataset_size = len(dataset['inputs']) + if dataset_size == 0: + print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') + return [], [] + + actual_batch_size = min(batch_size, dataset_size) + indices = np.random.choice(dataset_size, actual_batch_size, replace=False) + xs = [dataset['inputs'][i] for i in indices] + infos = [dataset['infos'][i] for i in indices] + return xs, infos + + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], + dataset: Dict[str, List[Any]], # Changed from validate_dataset + guide, # Changed from validate_guide + evaluation_batch_size: int, # New parameter name + num_threads: Optional[int] = None + ) -> Tuple[float, int]: + """Evaluates a given set of parameters on samples from the provided dataset (now typically train_dataset).""" + if not dataset or not dataset.get('inputs') or not dataset.get('infos') or not dataset['inputs']: + print_color("Evaluation dataset is empty or invalid. Returning score -inf, count 0.", color='yellow') + return -np.inf, 0 + + original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + self.optimizer.update(params_to_eval_dict) + + eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size + + if not eval_xs: + print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') + self.optimizer.update(original_params) + return -np.inf, 0 + + eval_scores = evaluate(self.agent, + guide, # Use main guide + eval_xs, + eval_infos, + min_score=self.min_score if hasattr(self, 'min_score') else None, + num_threads=num_threads or self.num_threads, + description=f"Evaluating candidate") + + self.optimizer.update(original_params) + + avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf + eval_count = len(eval_xs) + + return float(avg_score), eval_count + + def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: + """Calculates UCB score for a candidate in the buffer.""" + if candidate_buffer_entry['eval_count'] == 0: + return float('inf') # Explore unvisited states first + + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] + + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall + # and to ensure log argument is > 0. + # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. + if total_tracked_evaluations == 0: # Should not happen if we init with one eval + total_tracked_evaluations = 1 + + # UCB exploration term: ucb_exploration_factor scales the confidence interval + # Higher factor = more exploration, lower factor = more exploitation + exploration_term = self.ucb_exploration_factor * \ + math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) + + return mean_score + exploration_term + + def _update_buffer_ucb_scores(self): + """Recalculates and updates UCB scores for all candidates in the buffer.""" + if not self.buffer: + return + + for candidate_entry in self.buffer: + candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) + + def train(self, + guide, # Guide for train_dataset (feedback generation AND evaluation) + train_dataset: Dict[str, List[Any]], + *, + validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset + num_search_iterations: int = 100, + train_batch_size: int = 2, + evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations + eval_frequency: int = 1, + log_frequency: Optional[int] = None, + save_frequency: Optional[int] = None, + save_path: str = "checkpoints/ucb_agent.pkl", + min_score_for_agent_update: Optional[float] = None, # Renamed from min_score to avoid conflict with evaluate's min_score + verbose: Union[bool, str] = False, + num_threads: Optional[int] = None, + **kwargs + ) -> Tuple[Dict[str, Any], float]: # Returns metrics and best score + """ + Main training loop for UCB Search Algorithm. + """ + # Default validation_dataset to train_dataset if not provided + if validation_dataset is None: + validation_dataset = train_dataset + + num_threads = num_threads or self.num_threads + log_frequency = log_frequency or eval_frequency + self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate + total_samples = 0 + + # Metrics tracking + metrics = { + 'best_candidate_scores': [], # Score of the best candidate (e.g., highest mean) found so far at each iteration + 'selected_action_ucb': [], # UCB score of the selected action 'a' + 'new_candidate_scores': [], # Score of the new candidate 'a_prime' + 'buffer_avg_score': [], + 'buffer_avg_evals': [], + } + +# 0. Evaluate the initial parameter on samples of the validation set and add it to the buffer. + print_color("Evaluating initial parameters using validation_dataset samples...", 'cyan') + initial_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + initial_score, initial_evals = self._evaluate_candidate( + initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide + ) + self._total_evaluations_tracker += initial_evals + total_samples += initial_evals + + # Log initial evaluation + self.logger.log('Initial UCB score', initial_score, 0, color='blue') + self.logger.log('Initial evaluations', initial_evals, 0, color='cyan') + + initial_candidate_entry = { + 'params': initial_params_dict, + 'score_sum': initial_score * initial_evals if initial_score > -np.inf else 0, # Store sum for accurate mean later + 'eval_count': initial_evals, + 'ucb_score': None, # avoid accidental reads before it's initialized + 'iteration_created': 0 + } + self.buffer.append(initial_candidate_entry) + self._update_buffer_ucb_scores() # Update UCB for the initial candidate + print_color(f"Initial candidate: Score {initial_score:.4f}, Evals {initial_evals}", 'yellow') + + # Main search loop + for iteration in range(1, num_search_iterations + 1): + if not self.buffer: + print_color("Buffer is empty, stopping search.", 'red') + break + + # 1. Pick the candidate 'a' with the highest UCB from the buffer + self._update_buffer_ucb_scores() # Ensure UCB scores are fresh + action_candidate_a = self.select(self.buffer) + + # Log selected action UCB score + self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') + self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') + + print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') + + + # 2. Load parameters of 'a' into the agent for the optimizer update step + self.optimizer.update(action_candidate_a['params']) + + # 3. Draw minibatch from the training set, do update from 'a' to get 'a_prime' + train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) + if not train_xs: + print_color(f"Iter {iteration}: Training minibatch empty, skipping optimizer step.", 'yellow') + continue + + # Perform forward pass and get feedback for agent parameters 'a' + outputs_for_a = [] + use_asyncio = self._use_asyncio(num_threads) + if use_asyncio: + outputs_for_a = async_run([self.forward]*len(train_xs), + [(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)], + max_workers=num_threads, + description=f"Iter {iteration}: Forward pass for action 'a' ") + else: + outputs_for_a = [self.forward(self.agent, x, guide, info) for x, info in zip(train_xs, train_infos)] + + scores_from_train, targets_from_train, feedbacks_from_train = [], [], [] + for target, score, feedback in outputs_for_a: + scores_from_train.append(score) + targets_from_train.append(target) + feedbacks_from_train.append(feedback) + + if not scores_from_train: # Should not happen if train_xs was not empty + print_color(f"Iter {iteration}: No outputs from forward pass for candidate 'a'. Skipping.", 'yellow') + continue + + target_for_a = batchify(*targets_from_train) + feedback_for_a = batchify(*feedbacks_from_train).data + score_for_a_on_train_batch = np.mean([s for s in scores_from_train if s is not None]) if any(s is not None for s in scores_from_train) else -np.inf + + self.optimizer.zero_feedback() + self.optimizer.backward(target_for_a, feedback_for_a) # Grads for 'a' are now in optimizer + + try: + a_prime_params_dict = self.optimizer.step(bypassing=True, verbose='output') + if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: + print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') + # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime + a_prime_params_dict = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} + + except Exception as e: + print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') + continue + + # 4. Evaluate 'a_prime' on samples of validation set + a_prime_score, a_prime_evals = self._evaluate_candidate( + a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide + ) + self._total_evaluations_tracker += a_prime_evals + total_samples += evaluation_batch_size + train_batch_size + metrics['new_candidate_scores'].append(a_prime_score) + + # Log new candidate performance + self.logger.log('New candidate score', a_prime_score, iteration, color='green') + self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') + + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') + + # 5. Update the stats of 'a' (action_candidate_a) based on the training batch experience + if score_for_a_on_train_batch > -np.inf: + action_candidate_a['score_sum'] += score_for_a_on_train_batch * len(train_xs) # score is often an average + action_candidate_a['eval_count'] += len(train_xs) # or 1 if score is total + self._total_evaluations_tracker += len(train_xs) # training batch also counts as evaluations for UCB total T + + # 6. Add 'a_prime' (with its validation stats) to the buffer + if a_prime_score > -np.inf and a_prime_evals > 0: + new_candidate_entry = { + 'params': a_prime_params_dict, + 'score_sum': a_prime_score * a_prime_evals, # Store sum + 'eval_count': a_prime_evals, + 'ucb_score': None, # avoid accidental reads before it's initializad + 'iteration_created': iteration + } + + # Eviction logic before adding if buffer is at max_len + if len(self.buffer) == self.max_buffer_size: + self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction + candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) + self.buffer.remove(candidate_to_evict) + print_color(f"Iter {iteration}: Buffer full. Evicted a candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') + + self.buffer.append(new_candidate_entry) + print_color(f"Iter {iteration}: Added new candidate to buffer.", 'magenta') + else: + print_color(f"Iter {iteration}: New candidate a_prime had invalid score/evals, not added to buffer.", 'yellow') + + # Update all UCB scores in the buffer after potential additions/removals/stat updates + self._update_buffer_ucb_scores() + + # Logging + best_in_buffer = max(self.buffer, key=lambda c: c['score_sum']/(c['eval_count'] or 1)) + metrics['best_candidate_scores'].append(best_in_buffer['score_sum']/(best_in_buffer['eval_count'] or 1)) + metrics['buffer_avg_score'].append(np.mean([c['score_sum']/(c['eval_count'] or 1) for c in self.buffer if c['eval_count'] > 0])) + metrics['buffer_avg_evals'].append(np.mean([c['eval_count'] for c in self.buffer])) + + if iteration % log_frequency == 0: + log_data = { + "iteration": iteration, + "best_score": metrics['best_candidate_scores'][-1], #best_candidate_score_in_buffer + "selected_action_ucb": action_candidate_a['ucb_score'], + "new_candidate_score": a_prime_score, + "buffer_size": len(self.buffer), + "buffer_avg_score": metrics['buffer_avg_score'][-1], + "buffer_avg_evals": metrics['buffer_avg_evals'][-1], + "total_evaluations_tracker": self._total_evaluations_tracker, + "total_samples": total_samples # Add new metric + } + + # Log all important metrics + self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') + self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') + self.logger.log('Buffer average score', log_data['buffer_avg_score'], iteration, color='cyan') + self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') + self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') + self.logger.log('Total samples processed', log_data['total_samples'], iteration, color='yellow') + + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') + + # Save agent (e.g., the one with highest mean score in buffer) + if save_frequency is not None and iteration % save_frequency == 0: + best_overall_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9) ) + self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer + self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase + print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') + + # End of search loop + print_color("UCB search finished.", 'blue') + + # Log final training summary + final_iteration = num_search_iterations + self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') + self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') + + if not self.buffer: + print_color("Buffer is empty at the end of search. No best candidate found.", 'red') + self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') + return metrics, -np.inf + + # Select the best candidate based on highest mean score (exploitation) + final_best_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) + final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) + + # Log final results + self.logger.log('Final best score', final_best_score, final_iteration, color='green') + self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') + self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') + + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') + + # Load best parameters into the agent + self.optimizer.update(final_best_candidate['params']) # Load params using optimizer + + return metrics, float(final_best_score) + + def select(self, buffer): + '''Could be subclassed to implement different selection strategies''' + return max(buffer, key=lambda c: c['ucb_score']) \ No newline at end of file From 5625a9a0f08924dd86c5deadfdc69585ed3cc26f Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 16 Jul 2025 23:06:18 +0000 Subject: [PATCH 127/172] Fix the bug of validate. --- opto/trainer/algorithms/search_algorithms.py | 69 +++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index fd8eb1bc..79eca888 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -67,14 +67,14 @@ def remap_update_dict(base_module, update_dict): """ parameters = base_module.parameters() # get the parameters of the base agent remapped_update_dict = {} - for k, v in update_dict.items(): - for p in parameters: - # Check if k is a copy of p or p is a copy of k + # if fill_missing: + # remap all keys of the base_module's parameters and those in update_dict will be filled with their values in update_dict + for p in parameters: + remapped_update_dict[p] = p.data + for k, v in update_dict.items(): if is_node_copy(k, p): - k = p # remap k to the original parameter - remapped_update_dict[k] = v # set the value in the remapped update dict - break # stop checking once we've found a match - # remapped_update_dict is empty if no keys in update_dict matched any parameters of the base_module + remapped_update_dict[p] = v + break # stop checking once we've found a match return remapped_update_dict def set_module_parameters(agent, update_dict): @@ -326,12 +326,15 @@ def __init__(self, assert isinstance(base_module, trace.Module), "base_module must be a trace.Module." self.base_module = base_module self.update_dict = update_dict if update_dict is not None else {} + self.update_dict = remap_update_dict(self.base_module, self.update_dict) self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) def get_module(self): - """ Apply the update_dict to the base_module and return the updated module. This will not update the base_module itself.""" - module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) - module._ModuleCandidate_candidate_id = id(self) # set the id of the module to the id of the candidate; this is used to identify the candidate in the priority queue + """ Apply the update_dict to the base_module and return the updated module. + A new module is always created so the base_module is not modified. + The new module has a new attribute _module_candidate which is this candidate.""" + module = create_module_from_update_dict(self.base_module, self.update_dict) if self.update_dict else copy.deepcopy(self.base_module) # + setattr(module, '__TRACE_RESERVED_module_candidate_id', id(self)) return module # return the updated module def apply_update(self, base_module=None): @@ -345,7 +348,7 @@ def __deepcopy__(self, memo): memo[id(self)] = result for k, v in self.__dict__.items(): if k != 'base_module': - setattr(result, k, deepcopy(v, memo)) + setattr(result, k, copy.deepcopy(v, memo)) else: setattr(result, k, v) # base_module is not copied, it is the original module return result @@ -353,11 +356,11 @@ def __deepcopy__(self, memo): def __eq__(self, other): """ Check if two candidates are equal based on their base_module and update_dict. """ assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." - if self.base_module != other.base_module: - return False - update_dict_self = remap_update_dict(self.base_module, self.update_dict) - update_dict_other = remap_update_dict(other.base_module, other.update_dict) - return update_dict_self == update_dict_other + return self.update_dict == other.update_dict + + def __hash__(self): + """ Hash the candidate based on its update_dict. """ + return hash(frozenset(self.update_dict.items())) def add_rollouts(self, rollouts: List[Dict[str, Any]]): """ Add rollouts to the candidate. """ @@ -551,18 +554,36 @@ def validate(self, candidates, samples, verbose=False, **kwargs): # In validate_samples, there may be multiple rollouts collected by the same agent (or their copies). # We need to group the rollouts by the agent (ModuleCandidate) and return a dictionary where the keys are the ModuleCandidate objects and the values are lists of rollouts (list of dicts). - results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) - for c in candidates + exploration_candidates: - # Initialize the candidate in the results dictionary - results[id(c)] = (c, []) # (ModuleCandidate, list of rollouts) + # Group the samples by the ModuleCandidate id + _results = {} # dict of ModuleCandidate: list of rollouts (list of dicts) + for c in exploration_candidates + candidates: + _results[id(c)] = [] for rollouts in validate_samples.samples: module = rollouts.module # trace.Module - key = module._ModuleCandidate_candidate_id # use the candidate id as the key - if key not in results: + key = getattr(module, '__TRACE_RESERVED_module_candidate_id') # use the candidate as the key + if key not in _results: raise ValueError(f"ModuleCandidate with id {key} not found in results. Samples are not collected by known candidates.") # Append the rollouts to the list of rollouts for the key - results[key][1].extend(rollouts.to_list()) + _results[key].extend(rollouts.to_list()) + + # Merge rollouts of ModuleCandidates sharing the same parameters + results = {} # dict of ModuleCandidate id: (ModuleCandidate, list of rollouts) + for c in exploration_candidates + candidates: + rollouts_list = _results[id(c)] + matched = False + for k in results.keys(): + if k == c: + matched = True + if id(k) != id(c): # merging rollouts of candidates with the same parameters + rollouts_list += c.rollouts + results[k].extend(rollouts_list) # add the rollouts to the candidate + break + if not matched: # key not found in results + results[c] = rollouts_list # add the rollouts to the candidate + + # NOTE what if propose creates multiple exploration_candidates that have the same parameters and the same rollouts stats? + # For example, it copies candidates. This would create a bug. return results @@ -573,7 +594,7 @@ def update_memory(self, validate_results, **kwargs): validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. **kwargs: Additional keyword arguments that may be used by the implementation. """ - for candidate_id, (candidate, rollouts) in validate_results.items(): + for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate heapq.heappush(self.memory, (-score, candidate)) # add the candidate to the priority queue From f1cf63c3c2ab27193a3f490dbb822d4a70b37954 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Wed, 16 Jul 2025 20:52:40 -0500 Subject: [PATCH 128/172] Fix an issue about missing parameters in the proposed update_dict --- opto/trainer/algorithms/search_algorithms.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 36d71730..61716d79 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -67,12 +67,9 @@ def remap_update_dict(base_module, update_dict): """ parameters = base_module.parameters() # get the parameters of the base agent remapped_update_dict = {} - # if fill_missing: - # remap all keys of the base_module's parameters and those in update_dict will be filled with their values in update_dict - for p in parameters: - remapped_update_dict[p] = p.data - for k, v in update_dict.items(): - if is_node_copy(k, p): + for k, v in update_dict.items(): + for p in parameters: + if is_node_copy(k, p): # Check if k is a copy of p or p is a copy of k remapped_update_dict[p] = v break # stop checking once we've found a match return remapped_update_dict @@ -497,6 +494,10 @@ def _step(n, verbose=False, num_threads=None, **kwargs): optimizer.zero_feedback() # reset the optimizer's feedback optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks update_dict = optimizer.step(verbose=verbose, num_threads=num_threads, bypassing=True, **kwargs) + # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters + for param in optimizer.parameters: # for all parameters + if param not in update_dict: # update_dict misses some parameters + update_dict[param] = param.data # add the parameter to the update_dict # the update_dict is linked to the copied parameters of the agent, we set it back to the agent's parameters update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters return update_dict # return the proposed parameters From c47d4e5cc239d4fdc6ae81fe61cd8825b8b6e710 Mon Sep 17 00:00:00 2001 From: Xuanfei Ren Date: Wed, 16 Jul 2025 21:48:04 -0500 Subject: [PATCH 129/172] Revert "add ucb reward" This reverts commit 166c9781fac815a979c38f9d9bb1410b073bb2c7. --- examples/gsm8k_search_algo.py | 15 ++++--- opto/trainer/algorithms/search_algorithms.py | 45 -------------------- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py index 1142e586..1243cd8f 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/gsm8k_search_algo.py @@ -3,8 +3,8 @@ from opto import trace from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrime -from opto.trainer.algorithms.search_algorithms import UCBSearch as SearchAlgorithm -from opto.trainer.loggers import WandbLogger +from opto.trainer.algorithms.search_algorithms import PrioritySearch as SearchAlgorithm +from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any @@ -47,7 +47,8 @@ def forward(self, message: Any) -> Any: Guide = VerbalJudgeGuide -Logger = WandbLogger +Logger = TensorboardLogger + def main(): # set seed @@ -61,9 +62,9 @@ def main(): num_threads = 10 datasize = 5 verbose = True - teacher_model = "vertex_ai/gemini-2.0-flash" # use default model - student_model = "vertex_ai/gemini-2.0-flash" # use default model - optimizer_model = "vertex_ai/gemini-2.0-flash" # use default model + teacher_model = None # use default model + student_model = None # use default model + optimizer_model = None # use default model np.random.seed(seed) @@ -76,7 +77,7 @@ def main(): agent = Learner(llm=LLM(student_model)) guide = Guide(llm=LLM(teacher_model)) optimizer = OptoPrime(agent.parameters(), llm=LLM(optimizer_model)) - logger = Logger(project="gsm8k-examples", name="ucb",verbose=verbose) + logger = Logger(verbose=verbose) # set use_json_object_format=False if LLM does not support JSON object format alg = SearchAlgorithm( diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 61716d79..02653c12 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -657,48 +657,3 @@ def compute_score(self, candidate): default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates return np.mean(scores) if scores else self.default_score - -class UCBSearch(PrioritySearch): - """A search algorithm that keeps a buffer with candidates and their UCB scores. It does exploration according to the UCB score.""" - - def __init__(self, *args, exploration_constant=1.0, **kwargs): - """Initialize UCBSearch with an exploration constant for the UCB formula.""" - super().__init__(*args, **kwargs) - self.exploration_constant = exploration_constant - - def compute_score(self, candidate): - """Compute the UCB score for the candidate. - - UCB = mean_score + exploration_constant * sqrt(ln(total_trials) / candidate_trials) - - Args: - candidate (ModuleCandidate): The candidate for which to compute the UCB score. - Returns: - float: The computed UCB score for the candidate. - """ - if not isinstance(candidate, ModuleCandidate): - raise TypeError("candidate must be an instance of ModuleCandidate.") - - # Get scores from rollouts - scores = [r['score'] for r in candidate.rollouts] - - # If no rollouts, return a high exploration score to encourage trying this candidate - if not scores: - return float('inf') # Maximum exploration for untried candidates - - # Calculate mean score for this candidate - mean_score = np.mean(scores) - candidate_trials = len(scores) - - # Calculate total trials across all candidates in memory - total_trials = sum(len(c.rollouts) for _, c in self.memory) - - # Handle edge case where total_trials is 0 or 1 - if total_trials <= 1: - return mean_score - - # Calculate UCB score - exploration_term = self.exploration_constant * np.sqrt(np.log(total_trials) / candidate_trials) - ucb_score = mean_score + exploration_term - - return ucb_score From 67433190b23594e3c7379dcffdc119f31f1b4f6a Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 22 Jul 2025 23:29:39 +0000 Subject: [PATCH 130/172] Add memory size and ignore empty update. --- opto/trainer/algorithms/search_algorithms.py | 65 ++++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 02653c12..4b5691ec 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -18,10 +18,8 @@ # TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate # TODO a better data structure to store samples -# update_dict - -# Some helper function to convert between trace.Module and update_dict +# Some helper functions to convert between trace.Module and update_dict def get_original_name(node): """Extract the original name from a node, removing all _copy suffixes.""" @@ -153,8 +151,8 @@ def train(self, num_threads = None, # maximum number of threads to use verbose = False, # whether to print the output of the agent # evaluation - test_dataset = None, # dataset of (x, info) pairs to evaluate the agent - test_guide = None, # guide to provide scores for the test set + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent; if None, use train_dataset + test_guide = None, # guide to provide scores for the test set; if None, use guide eval_frequency: Union[int, None] = 1, # frequency of evaluation num_eval_samples: int = 1, # number of samples to use to evaluate each input # logging @@ -165,14 +163,13 @@ def train(self, ): ## Setup - test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided test_guide = test_guide or guide self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input - self.score_range = score_range or (0., 1.) + self.score_range = score_range or (-np.inf, np.inf) self.train_sampler = Sampler( DataLoader(train_dataset, batch_size=batch_size), @@ -376,6 +373,45 @@ def score(self): scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None +class HeapMemory: + # This is a basic implementation of a heap memory that uses a priority queue to store candidates. + # Later on this will be replaced by a memory DB. + def __init__(self, size=None): + """ Initialize an empty heap memory. """ + self.memory = [] + self.size = size # Optional size limit for the heap memory + + def push(self, item): + """ Push an item to the heap memory. """ + heapq.heappush(self.memory, item) + if self.size is not None and len(self.memory) > self.size: + # NOTE a heuristic for now + self.memory = self.memory[:self.size] # Keep only the top `size` items + + def pop(self): + """ Pop the top item from the heap memory. """ + if not self.memory: + raise IndexError("pop from an empty heap memory") + return heapq.heappop(self.memory) + + def __len__(self): + """ Return the number of items in the heap memory. """ + return len(self.memory) + + def __bool__(self): + """ Return True if the heap memory is not empty, False otherwise. """ + return len(self.memory) > 0 + + def __iter__(self): + """ Iterate over the items in the heap memory. """ + return iter(self.memory) + + def best(self): + """ Return the best item in the heap memory without removing it. """ + if not self.memory: + raise IndexError("best from an empty heap memory") + return self.memory[0] + class PrioritySearch(SearchTemplate): """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. """ @@ -406,6 +442,7 @@ def train(self, num_candidates: int = 10, # number of candidates to propose default_score: float = float('inf'), # default score assigned to priority queue candidates validate_proposals: bool = True, # whether to validate the proposed parameters + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set # Additional keyword arguments **kwargs ): @@ -414,7 +451,9 @@ def train(self, self.num_candidates = num_candidates # number of candidates to propose by each optimizer call self.validate_proposals = validate_proposals # whether to validate the proposed parameters self.default_score = default_score - self.memory = [(self.default_score, ModuleCandidate(self.agent))] # Priority queue of ModuleCandidates, initialized with the base agent + self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit + self.memory.push((self.default_score, ModuleCandidate(self.agent))) # Push the base agent as the first candidate + self._exploration_candidates = None super().train(guide, train_dataset, @@ -494,6 +533,8 @@ def _step(n, verbose=False, num_threads=None, **kwargs): optimizer.zero_feedback() # reset the optimizer's feedback optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks update_dict = optimizer.step(verbose=verbose, num_threads=num_threads, bypassing=True, **kwargs) + if not update_dict: # if the optimizer did not propose any updates + return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters for param in optimizer.parameters: # for all parameters if param not in update_dict: # update_dict misses some parameters @@ -513,7 +554,7 @@ def _step(n, verbose=False, num_threads=None, **kwargs): description="Running optimizers on samples") # update_dicts is a list of dicts of length n_agents * n_proposals # Create ModuleCandidate objects for each proposed update_dict - candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts] + candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts if update_dict is not None] # filter out None updates return candidates def validate(self, candidates, samples, verbose=False, **kwargs): @@ -598,7 +639,7 @@ def update_memory(self, validate_results, **kwargs): for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate - heapq.heappush(self.memory, (-score, candidate)) # add the candidate to the priority queue + self.memory.push((-score, candidate)) # push the candidate to the priority queue with negative score (to make it a max-heap) #### @@ -613,7 +654,7 @@ def explore(self, **kwargs): # pop top self.num_candidates candidates from the priority queue top_candidates = [] while len(top_candidates) < self.num_candidates and self.memory: - score, candidate = heapq.heappop(self.memory) + score, candidate = self.memory.pop() # pop the top candidate from the priority queue top_candidates.append(candidate) # add the candidate to the top candidates return top_candidates, {} @@ -630,7 +671,7 @@ def exploit(self, **kwargs): # This function can be overridden by subclasses to implement a different exploitation strategy if not self.memory: raise ValueError("The priority queue is empty. Cannot exploit.") - best = min(self.memory) # (score, candidate) + best = self.memory.best() # (score, candidate) score, best_candidate = best score = -score # remember that we stored negative scores in the priority queue return best_candidate, { From 202c10d383669974b787eca5aaa4edaa8e5b67f3 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 21:17:55 +0000 Subject: [PATCH 131/172] Fix a bug in propose in PrioritySearch. Add __lt__ method to ModuleCandidate. Add test for priority search. --- opto/trainer/algorithms/search_algorithms.py | 62 +++++--- tests/unit_tests/test_priority_search.py | 158 +++++++++++++++++++ tests/unit_tests/test_sampler.py | 11 +- 3 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 tests/unit_tests/test_priority_search.py diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index 4b5691ec..b7c644b5 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -10,8 +10,8 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify from opto.trainer.evaluators import evaluate from opto.trainer.loader import DataLoader - from opto.trainer.sampler import Sampler, RolloutsGraph +import time # TODO save and load SearchTemplate # TODO async version??? @@ -322,6 +322,7 @@ def __init__(self, self.update_dict = update_dict if update_dict is not None else {} self.update_dict = remap_update_dict(self.base_module, self.update_dict) self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) + self.created_time = time.time() def get_module(self): """ Apply the update_dict to the base_module and return the updated module. @@ -352,6 +353,14 @@ def __eq__(self, other): assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." return self.update_dict == other.update_dict + # TODO better way? + def __lt__(self, other): + """ Compare two candidates based on their update_dict. """ + assert isinstance(other, ModuleCandidate), "other must be an instance of ModuleCandidate." + return self.created_time > other.created_time + # This would give priority to later created candidates in the heap memory + # since the heapq is a min-heap . + def __hash__(self): """ Hash the candidate based on its update_dict. """ return hash(frozenset(self.update_dict.items())) @@ -376,14 +385,16 @@ def score(self): class HeapMemory: # This is a basic implementation of a heap memory that uses a priority queue to store candidates. # Later on this will be replaced by a memory DB. + + # NOTE that the heap memory is a max-heap, so we store negative scores to use the default min-heap behavior of heapq. def __init__(self, size=None): """ Initialize an empty heap memory. """ self.memory = [] self.size = size # Optional size limit for the heap memory - def push(self, item): + def push(self, score, data): """ Push an item to the heap memory. """ - heapq.heappush(self.memory, item) + heapq.heappush(self.memory, (-score, data)) if self.size is not None and len(self.memory) > self.size: # NOTE a heuristic for now self.memory = self.memory[:self.size] # Keep only the top `size` items @@ -439,7 +450,8 @@ def train(self, save_frequency: Union[int, None] = None, # frequency of saving the agent save_path: str = "checkpoints/agent.pkl", # path to save the agent # Priority Search specific parameters - num_candidates: int = 10, # number of candidates to propose + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer default_score: float = float('inf'), # default score assigned to priority queue candidates validate_proposals: bool = True, # whether to validate the proposed parameters memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set @@ -449,10 +461,11 @@ def train(self, # Create agents and optimizers for search self.num_candidates = num_candidates # number of candidates to propose by each optimizer call + self.num_proposals = num_proposals self.validate_proposals = validate_proposals # whether to validate the proposed parameters self.default_score = default_score self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit - self.memory.push((self.default_score, ModuleCandidate(self.agent))) # Push the base agent as the first candidate + self.memory.push(self.default_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate self._exploration_candidates = None @@ -497,7 +510,7 @@ def update(self, samples=None, verbose=False, **kwargs): info_log.update(info_explore) # add the info from the explore step return best_candidate.update_dict, [c.get_module() for c in exploration_candidates], info_log - def propose(self, samples, verbose=False, n_proposals=1, **kwargs): + def propose(self, samples, verbose=False, **kwargs): """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. Args: @@ -512,18 +525,13 @@ def propose(self, samples, verbose=False, n_proposals=1, **kwargs): assert isinstance(samples, Samples), "samples must be an instance of Samples." samples = samples.samples # list of RolloutsGraph objects + n_proposals = self.num_proposals # number of proposals to generate per optimizer - def _step(n, verbose=False, num_threads=None, **kwargs): - """ Standard optimizer step for a single agent. """ - # optimizer = self._optimizers[n] # get the optimizer for the n-th agent - # NOTE this seems slow + def _backward(n): optimizer = copy.deepcopy(self.optimizer) # create a copy of the optimizer to avoid modifying the original one - rollouts = samples[n] # RolloutsGraph - # Make sure all rollouts are based on the same module, so they can be viewed as a minibatch. optimizer.parameters = rollouts.module.parameters() # set the optimizer's parameters to the proposal's parameters - targets = [r.target for r in rollouts] feedbacks = [r.feedback for r in rollouts] # batchify the targets and feedbacks @@ -532,7 +540,18 @@ def _step(n, verbose=False, num_threads=None, **kwargs): # standard optimizer step optimizer.zero_feedback() # reset the optimizer's feedback optimizer.backward(target, feedback) # compute the gradients based on the targets and feedbacks - update_dict = optimizer.step(verbose=verbose, num_threads=num_threads, bypassing=True, **kwargs) + return optimizer + + n_subgraphs = len(samples) # number of subgraphs (agents) in the samples + args_list = [(n,) for n in range(n_subgraphs)] + optimizers = async_run([_backward]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel + args_list=args_list, + max_workers=self.num_threads, # use the number of threads specified in the class + description=None) + + # For each optimizer, containing the backward feedback, we call it n_proposals times to get the proposed parameters. + def _step(optimizer): + update_dict = optimizer.step(verbose=verbose, num_threads=self.num_threads, bypassing=True, **kwargs) if not update_dict: # if the optimizer did not propose any updates return None # return None to indicate no updates were proposed # update_dict may only contain some of the parameters of the agent, we need to make sure it contains all the parameters @@ -543,15 +562,12 @@ def _step(n, verbose=False, num_threads=None, **kwargs): update_dict = remap_update_dict(self.agent, update_dict) # remap the update dict to the agent's parameters return update_dict # return the proposed parameters - n_subgraphs = len(samples) # number of subgraphs (agents) in the samples - args_list = [(n, verbose, self.num_threads) for n in range(n_subgraphs)] - args_list = args_list * n_proposals # repeat args_list n_proposals times - kwargs_list = [kwargs] * n_subgraphs * n_proposals # repeat kwargs for each agent + args_list = [(o,) for o in optimizers ] * n_proposals # repeat args_list n_proposals times update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, - kwargs_list=kwargs_list, max_workers=self.num_threads, # use the number of threads specified in the class description="Running optimizers on samples") + # update_dicts is a list of dicts of length n_agents * n_proposals # Create ModuleCandidate objects for each proposed update_dict candidates = [ModuleCandidate(self.agent, update_dict) for update_dict in update_dicts if update_dict is not None] # filter out None updates @@ -623,7 +639,6 @@ def validate(self, candidates, samples, verbose=False, **kwargs): break if not matched: # key not found in results results[c] = rollouts_list # add the rollouts to the candidate - # NOTE what if propose creates multiple exploration_candidates that have the same parameters and the same rollouts stats? # For example, it copies candidates. This would create a bug. return results @@ -639,8 +654,7 @@ def update_memory(self, validate_results, **kwargs): for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate - self.memory.push((-score, candidate)) # push the candidate to the priority queue with negative score (to make it a max-heap) - + self.memory.push(score, candidate) #### def explore(self, **kwargs): @@ -648,8 +662,8 @@ def explore(self, **kwargs): Args: **kwargs: Additional keyword arguments that may be used by the implementation. Returns: - update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. - proposal_update_dicts (list of dict): A list of proposed parameter updates (dict) for the next iteration. + list: A list of proposed candidates. + dict: A dictionary containing logging information about the exploration. """ # pop top self.num_candidates candidates from the priority queue top_candidates = [] diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py new file mode 100644 index 00000000..bf331e40 --- /dev/null +++ b/tests/unit_tests/test_priority_search.py @@ -0,0 +1,158 @@ +from opto import trace +from opto.trainer.loader import DataLoader +from opto.trainer.sampler import Sampler +from opto.trainer.algorithms.search_algorithms import PrioritySearch as _PrioritySearch +from opto.trainer.algorithms.search_algorithms import ModuleCandidate +from opto.optimizers import OptoPrimeV2 +from opto.trainer.guide import AutoGuide +from opto.utils.llm import DummyLLM + +import re +import numpy as np + + +class Guide(AutoGuide): + + def get_feedback(self, query, response, reference=None, **kwargs): + """ + Provide feedback based on the query and response. + + Args: + query: The query to analyze. + response: The response generated by the model. + reference: Optional reference answer for comparison. + **kwargs: Additional context or parameters. + + Returns: + A tuple containing a score and feedback string. + """ + score = response == reference + feedback = "Exact match!" if score == 1.0 else "Not an exact match." + return score, feedback + +@trace.model +class Agent: + + def __init__(self): + self.param = trace.node(1., trainable=True) + self.state = 0 + + def forward(self, x): + return self.param + 1 + + + +xs = [1, 2, 3, 4, 5] +infos = [1, 2, 3, 4, 5] +batch_size = 3 +sub_batch_size = 2 +num_threads = 1 # 2 +dataset = {'inputs': xs, 'infos': infos} +loader = DataLoader(dataset, batch_size=batch_size, randomize=False) +sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) + +num_proposals = 10 +num_candidates = 5 +memory_size = 3 +suggested_value = 5 + + + +class PrioritySearch(_PrioritySearch): + # This class is for testing the PrioritySearch algorithm + + def propose(self, samples, verbose=False, n_proposals=1, **kwargs): + print("Propose at iteration:", self.n_iters) + # assert len(samples) == batch_size, f"Expected {batch_size} samples, got {len(samples)}" + # assert len(samples) == len(agents) * np.ceil(batch_size / self.sub_batch_size), f"Expected {len(agents) * np.ceil(batch_size / self.sub_batch_size)} samples, got {len(samples)}" + + candidates = super().propose(samples, verbose=verbose, n_proposals=n_proposals, **kwargs) + # In this example this will always be value 5 + assert isinstance(candidates, list), "Expected candidates to be a list" + assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" + assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" + return candidates + + def validate(self, candidates, samples, verbose=False, **kwargs): + print("Validate at iteration:", self.n_iters) + assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" + + validate_results = super().validate(candidates, samples, verbose=verbose, **kwargs) + assert isinstance(validate_results, dict), "Expected validate_results to be a dict" + assert all(isinstance(v, ModuleCandidate) for v in validate_results.keys()), "All keys should be ModuleCandidate instances" + keys = list(validate_results.keys()) + # should contain one from exploration and one from exploitation + assert len(validate_results) == 2, "In this example, all proposals are the same, so we expect only two validate results." + + return validate_results + + def exploit(self, **kwargs): + print("Exploit at iteration:", self.n_iters) + + candidate, info_dict = super().exploit(**kwargs) + assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" + assert isinstance(info_dict, dict), "Expected info_dict to be a dictionary" + return candidate, info_dict + + def explore(self, **kwargs): + print("Explore at iteration:", self.n_iters) + + candidates, info_dict = super().explore(**kwargs) + assert isinstance(candidates, list) + assert isinstance(info_dict, dict) + + if self.n_iters == 0: + assert len(candidates) == 1, f"Expected 1 candidate, got {len(candidates)}" + else: + num_candidates = min(self.num_candidates, 2) # in this example, memory will contain at most 2 unique candidates + assert len(candidates) == num_candidates, f"Expected {num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" + assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" + + return candidates, info_dict + + + +def _llm_callable(messages, **kwargs): + """ + A dummy LLM callable that simulates a response. + """ + problem = messages[1]['content'] + + # extract name from + name = re.findall(r"", problem) + if name: + name = name[0] + else: + name = "unknown" + + return f""" + Dummy reasoning based on the input messages. + + {name} + {suggested_value} + + """ + +dummy_llm = DummyLLM(_llm_callable) +agent = Agent() +optimizer = OptoPrimeV2( + agent.parameters(), + llm=dummy_llm, +) + +algo = PrioritySearch( + agent, + optimizer, +) + +algo.train( + guide=Guide(), + train_dataset=dataset, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + num_threads=num_threads, + num_candidates=num_candidates, + num_proposals=num_proposals, + memory_size=memory_size, + verbose=False, +) diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index 0ac4d104..d6fd6d16 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -66,6 +66,8 @@ def test_sample_with_single_agent(): for rollouts in samples: for rollout in rollouts: assert rollout.target == 1 # state is not affected by multiple calls + rollout.target.backward('Fake feedback') + # each rollout should be independent so `has been backwarded.` error should not be raised samples, batch = sampler.sample([Agent()]) @@ -79,7 +81,8 @@ def test_sample_with_single_agent(): for rollouts in samples: for rollout in rollouts: assert rollout.target == 1 # state is not affected by multiple calls - + rollout.target.backward('Fake feedback') + # each rollout should be independent so `has been backwarded.` error should not be raised def test_sample_with_multiple_agents(): """ @@ -116,7 +119,8 @@ def test_sample_with_multiple_agents(): for rollouts in samples: for rollout in rollouts: - assert rollout.target == 1 # state is not affected by multiple calls + rollout.target.backward('Fake feedback') + # each rollout should be independent so `has been backwarded.` error should not be raised samples, batch = sampler.sample([Agent(), Agent()]) # check batch is equal to dataset's second batch_size elements @@ -130,4 +134,5 @@ def test_sample_with_multiple_agents(): for rollouts in samples: for rollout in rollouts: - assert rollout.target == 1 # state is not affected by multiple calls \ No newline at end of file + rollout.target.backward('Fake feedback') + # each rollout should be independent so `has been backwarded.` error should not be raised \ No newline at end of file From 85b11313f5b0bac42da16e92efe42d8da265a931 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 21:21:51 +0000 Subject: [PATCH 132/172] Add num_rollouts property to ModuleCandidate --- opto/trainer/algorithms/search_algorithms.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/search_algorithms.py index b7c644b5..ed0b4e15 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/search_algorithms.py @@ -382,6 +382,11 @@ def score(self): scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None + @property + def num_rollouts(self): + """ Return the number of rollouts collected for this candidate. """ + return len(self.rollouts) + class HeapMemory: # This is a basic implementation of a heap memory that uses a priority queue to store candidates. # Later on this will be replaced by a memory DB. From 4e906cca84cf9e07470a1d6ae2dc847fd6ab5c83 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 21:35:22 +0000 Subject: [PATCH 133/172] Refactor priority search. --- examples/gsm8k_search_algo.py | 4 +- opto/trainer/algorithms/__init__.py | 1 + .../algorithms/priority_search/__init__.py | 1 + .../priority_search.py} | 302 +----------------- .../algorithms/priority_search/utils.py | 84 +++++ opto/trainer/sampler.py | 8 +- tests/unit_tests/test_priority_search.py | 6 +- 7 files changed, 97 insertions(+), 309 deletions(-) create mode 100644 opto/trainer/algorithms/priority_search/__init__.py rename opto/trainer/algorithms/{search_algorithms.py => priority_search/priority_search.py} (61%) create mode 100644 opto/trainer/algorithms/priority_search/utils.py diff --git a/examples/gsm8k_search_algo.py b/examples/gsm8k_search_algo.py index 1243cd8f..3e21f7cf 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/gsm8k_search_algo.py @@ -2,8 +2,8 @@ import numpy as np from opto import trace from opto.utils.llm import LLM, LiteLLM -from opto.optimizers import OptoPrime -from opto.trainer.algorithms.search_algorithms import PrioritySearch as SearchAlgorithm +from opto.optimizers import OptoPrimeV2 as OptoPrime +from opto.trainer.algorithms.priority_search import PrioritySearch as SearchAlgorithm from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index 2586fd31..084cd459 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1,3 +1,4 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm +from opto.trainer.algorithms.priority_search import PrioritySearch \ No newline at end of file diff --git a/opto/trainer/algorithms/priority_search/__init__.py b/opto/trainer/algorithms/priority_search/__init__.py new file mode 100644 index 00000000..68fe26c0 --- /dev/null +++ b/opto/trainer/algorithms/priority_search/__init__.py @@ -0,0 +1 @@ +from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch \ No newline at end of file diff --git a/opto/trainer/algorithms/search_algorithms.py b/opto/trainer/algorithms/priority_search/priority_search.py similarity index 61% rename from opto/trainer/algorithms/search_algorithms.py rename to opto/trainer/algorithms/priority_search/priority_search.py index ed0b4e15..10da4b89 100644 --- a/opto/trainer/algorithms/search_algorithms.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -1,306 +1,14 @@ import numpy as np import copy import heapq -from dataclasses import dataclass +import time from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace from opto.trace.nodes import ParameterNode -from opto.trainer.utils import async_run, batch_run -from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify -from opto.trainer.evaluators import evaluate -from opto.trainer.loader import DataLoader -from opto.trainer.sampler import Sampler, RolloutsGraph -import time - -# TODO save and load SearchTemplate -# TODO async version??? -# TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate -# TODO a better data structure to store samples - - -# Some helper functions to convert between trace.Module and update_dict - -def get_original_name(node): - """Extract the original name from a node, removing all _copy suffixes.""" - py_name = node.py_name # This removes colons: "param:0" -> "param0" - - # Find the first occurrence of "_copy" and remove it and everything after - copy_index = py_name.find('_copy') - if copy_index != -1: - return py_name[:copy_index] - else: - return py_name - -def is_node_copy(a, b): - """Check if two nodes are copies of each other by comparing their original names. - - This function has transitivity: if A is a copy of B and B is a copy of C, - then A is also considered a copy of C. - """ - return get_original_name(a) == get_original_name(b) - -def is_module_copy(a, b): - """ Check if a and b (trace.Modules) are copies of each other. """ - parameters_a = a.parameters() # list of ParameterNode - parameters_b = b.parameters() # list of ParameterNode - # Check if all parameters of a are copies of b or vice versa - # This might over count - # need to check 1:1 correspondence - matched = [] - for p_a in parameters_a: - _matched = [] - for p_b in parameters_b: - _matched.append(is_node_copy(p_a, p_b)) - np.array(matched) - if np.all(np.sum(matched, axis=1) == 1) and np.all(np.sum(matched, axis=0) == 1): - return True - return False - -def remap_update_dict(base_module, update_dict): - """ Remap the update dict to the agent's parameters. update_dict might have keys which are copies of the base_module's parameters or visa versa. - This function remaps the keys in update_dict to the original parameters of the base_module. - - The return dict is empty if no keys in update_dict matched any parameters of the base_module. This condition can be used to check if the update_dict contains non-trivial updates. - """ - parameters = base_module.parameters() # get the parameters of the base agent - remapped_update_dict = {} - for k, v in update_dict.items(): - for p in parameters: - if is_node_copy(k, p): # Check if k is a copy of p or p is a copy of k - remapped_update_dict[p] = v - break # stop checking once we've found a match - return remapped_update_dict - -def set_module_parameters(agent, update_dict): - """ Set the parameters of the agent based on the update_dict. - The update_dict is a dictionary of ParameterNode: value pairs. - The agent's parameters will be updated with the values from the update_dict. - """ - remapped_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters - for k, v in remapped_update_dict.items(): - k._data = v # set the parameter's data to the value in the update_dict - -def create_module_from_update_dict(agent, update_dict): - """ Create a new agent from the update_dict. - The update_dict is a dictionary of ParameterNode: value pairs. - A new agent will be created with the parameters set to the values from the update_dict. - """ - new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent - set_module_parameters(new_agent, update_dict) # set the parameters of the new agent - return new_agent # return the new agent - - - -class Samples: - """ A container for samples collected during the search algorithm. It contains a list of RolloutsGraph objects - and a dataset with inputs and infos which created the list of RolloutsGraph. """ - - samples: List[RolloutsGraph] - dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys - - def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): - assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." - assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." - assert isinstance(dataset, dict), "dataset must be a dict." - assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." - - self.samples = samples - self.dataset = dataset # NOTE this cannot be extracted from the samples in general? - - def add_samples(self, samples): - """ Add samples to the Samples object. """ - assert isinstance(samples, Samples), "samples must be an instance of Samples." - samples = samples.samples # extract the samples from the Samples object - assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." - assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." - - # TODO assert xs and infos are in self.minibatch - # add a function to extract unique inputs and infos from the samples - - self.samples.extend(samples) - - def get_batch(self): - return self.dataset #['inputs'], self.minibatch['infos'] - - def __iter__(self): - """ Iterate over the samples. """ - return iter(self.samples) - - def __len__(self): - return sum(len(s) for s in self.samples) - - - -class SearchTemplate(Minibatch): - # This only uses __init__ and evaluate of Minibatch class. - """ This implements a generic template for search algorithm. """ - - def train(self, - guide, # guide to provide feedback - train_dataset, # dataset of (x, info) pairs to train the agent - *, - # validation - validate_dataset = None, # same format as train_dataset; if None use the current batch. - validate_guide = None, # to provide scores for the validation set - # training loop - batch_size = 1, # batch size for updating the agent - sub_batch_size = None, # sub-batch size for broadcasting the agents - score_range = None, # minimum score to update the agent - num_epochs = 1, # number of training epochs - num_threads = None, # maximum number of threads to use - verbose = False, # whether to print the output of the agent - # evaluation - test_dataset = None, # dataset of (x, info) pairs to evaluate the agent; if None, use train_dataset - test_guide = None, # guide to provide scores for the test set; if None, use guide - eval_frequency: Union[int, None] = 1, # frequency of evaluation - num_eval_samples: int = 1, # number of samples to use to evaluate each input - # logging - log_frequency = None, # frequency of logging - save_frequency: Union[int, None] = None, # frequency of saving the agent - save_path: str = "checkpoints/agent.pkl", # path to save the agent - **kwargs - ): - - ## Setup - test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation - log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) - self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads - test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided - test_guide = test_guide or guide - self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input - self.score_range = score_range or (-np.inf, np.inf) - - self.train_sampler = Sampler( - DataLoader(train_dataset, batch_size=batch_size), - guide, - num_threads=self.num_threads, - sub_batch_size=sub_batch_size, - score_range=self.score_range - ) - self._validate_dataset = validate_dataset # if None, the current batch will be used for validation - self.validate_sampler = Sampler( - DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), - validate_guide or guide, - num_threads=self.num_threads, - sub_batch_size=None, # no sub-batch size for validation - score_range=self.score_range - ) - - # Evaluate the agent before learning - # NOTE set test_frequency < 0 to skip first evaluation - if (test_frequency is not None) and test_frequency > 0: - info_test = self.test(test_dataset, test_guide) # test self.agent - self.log(info_test) - - # Save the agent before learning if save_frequency > 0 - if (save_frequency is not None) and save_frequency > 0: - self.save(save_path) - - samples = None - self.n_epochs = 0 # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) - self.n_samples = 0 # number of training samples processed by the algorithm (This is incremented in sample) - train_scores = [] # to store the scores of the agent during training - - while self.n_epochs < num_epochs : - - print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") - - # 1. Propose new parameters given the current state of the algorithm - # proposals: list of trace.Modules - update_dict, proposals, info_update = self.update(samples, verbose=verbose, **kwargs) - self.optimizer.update(update_dict) # update self.agent with the proposed parameters - - # 2. Get feedback on the proposed parameters on the current batch - # samples: Samples object containing the samples and the minibatch - samples, info_sample = self.sample(proposals, verbose=verbose, **kwargs) - - # Evaluate the agent after update - if (test_frequency is not None) and (self.n_iters % test_frequency == 0): - info_test = self.test(test_dataset, test_guide) # test self.agent - self.log(info_test, prefix="Test: ") - - # Save the algorithm state - if (save_frequency is not None and save_frequency > 0) and self.n_iters % save_frequency == 0: - self.save(save_path) - - # Log information - assert 'mean_score' in info_sample, "info_sample must contain 'mean_score'." - assert 'n_epochs' in info_sample, "info_sample must contain 'n_epochs'." - - train_scores.append(info_sample['mean_score']) # so that mean can be computed - if self.n_iters % log_frequency == 0: - self.logger.log('Average train score', np.mean(train_scores), self.n_iters, color='blue') - self.log(info_update, prefix="Update: ") - self.log(info_sample, prefix="Sample: ") - self.n_samples += len(samples) # update the number of samples processed - self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') - # Log parameters - for p in self.agent.parameters(): - self.logger.log(f"Parameter: {p.name}", p.data, self.n_iters, color='red') - - # Update counters - self.n_epochs = info_sample['n_epochs'] # update the number of epochs completed - self.n_iters += 1 - return - - # Can be overridden by subclasses to implement specific sampling strategies - def sample(self, agents, verbose=False, **kwargs): - """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. - - Args: - agents (list): A list of trace.Modules (proposed parameters) to evaluate. - **kwargs: Additional keyword arguments that may be used by the implementation. - """ - samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch - - # Log information about the sampling - scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph - scores = [item for sublist in scores for item in sublist] # flatten the list of scores - log_info = { - 'mean_score': np.mean(scores), - 'n_epochs': self.train_sampler.n_epochs, - } - return samples, log_info - - def log(self, info_log, prefix=""): - """ Log the information from the algorithm. """ - for key, value in info_log.items(): - try: - if value is not None: - self.logger.log(f"{prefix}{key}", value, self.n_iters) - except Exception as e: - print(e) - - def test(self, test_dataset, guide): - min_score = self.score_range[0] - # Test the agent's performance - test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], - min_score=min_score, num_threads=self.num_threads, - description=f"Evaluating agent (iteration {self.n_iters})") # and log - return {'test_score': test_score} - - def save(self, save_path): - self.save_agent(save_path, self.n_iters) - # TODO save full state of self - - # Unimplemented methods that should be implemented by subclasses - def update(self, samples=None, verbose=False, **kwargs): - """ Update the agent based on the provided samples. - Args: - samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. - verbose (bool, optional): Whether to print verbose output. Defaults to False. - **kwargs: Additional keyword arguments that may be used by the implementation. - Returns: - update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. - proposals (list of trace.Module): A list of proposed parameters (trace.Module) after the update. - info_log (dict of str: Any): A dictionary containing logging information about the update process. - - This method updates the agent's parameters based on samples of the training dataset and validation dataset (provided by self.get_validate_dataset). - In addition, it return new agents (proposals) that can be used for collecting data for the next iteration. - """ - raise NotImplementedError("The update method should be implemented by subclasses.") - # return update_dict, proposals, info_log +from opto.trainer.utils import async_run +from opto.trainer.algorithms.basic_algorithms import batchify +from opto.trainer.algorithms.priority_search.search_template import SearchTemplate, Samples +from opto.trainer.algorithms.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict # TODO make this hashable? diff --git a/opto/trainer/algorithms/priority_search/utils.py b/opto/trainer/algorithms/priority_search/utils.py new file mode 100644 index 00000000..8c4ed9db --- /dev/null +++ b/opto/trainer/algorithms/priority_search/utils.py @@ -0,0 +1,84 @@ +import numpy as np +import copy +import heapq +from dataclasses import dataclass +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trace.nodes import ParameterNode +from opto.trainer.utils import async_run, batch_run +from opto.optimizers.utils import print_color +from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify +from opto.trainer.loader import DataLoader +from opto.trainer.sampler import Sampler, RolloutsGraph +import time + +# Some helper functions to convert between trace.Module and update_dict + +def get_original_name(node): + """Extract the original name from a node, removing all _copy suffixes.""" + py_name = node.py_name # This removes colons: "param:0" -> "param0" + + # Find the first occurrence of "_copy" and remove it and everything after + copy_index = py_name.find('_copy') + if copy_index != -1: + return py_name[:copy_index] + else: + return py_name + +def is_node_copy(a, b): + """Check if two nodes are copies of each other by comparing their original names. + + This function has transitivity: if A is a copy of B and B is a copy of C, + then A is also considered a copy of C. + """ + return get_original_name(a) == get_original_name(b) + +def is_module_copy(a, b): + """ Check if a and b (trace.Modules) are copies of each other. """ + parameters_a = a.parameters() # list of ParameterNode + parameters_b = b.parameters() # list of ParameterNode + # Check if all parameters of a are copies of b or vice versa + # This might over count + # need to check 1:1 correspondence + matched = [] + for p_a in parameters_a: + _matched = [] + for p_b in parameters_b: + _matched.append(is_node_copy(p_a, p_b)) + np.array(matched) + if np.all(np.sum(matched, axis=1) == 1) and np.all(np.sum(matched, axis=0) == 1): + return True + return False + +def remap_update_dict(base_module, update_dict): + """ Remap the update dict to the agent's parameters. update_dict might have keys which are copies of the base_module's parameters or visa versa. + This function remaps the keys in update_dict to the original parameters of the base_module. + + The return dict is empty if no keys in update_dict matched any parameters of the base_module. This condition can be used to check if the update_dict contains non-trivial updates. + """ + parameters = base_module.parameters() # get the parameters of the base agent + remapped_update_dict = {} + for k, v in update_dict.items(): + for p in parameters: + if is_node_copy(k, p): # Check if k is a copy of p or p is a copy of k + remapped_update_dict[p] = v + break # stop checking once we've found a match + return remapped_update_dict + +def set_module_parameters(agent, update_dict): + """ Set the parameters of the agent based on the update_dict. + The update_dict is a dictionary of ParameterNode: value pairs. + The agent's parameters will be updated with the values from the update_dict. + """ + remapped_update_dict = remap_update_dict(agent, update_dict) # remap the update dict to the agent's parameters + for k, v in remapped_update_dict.items(): + k._data = v # set the parameter's data to the value in the update_dict + +def create_module_from_update_dict(agent, update_dict): + """ Create a new agent from the update_dict. + The update_dict is a dictionary of ParameterNode: value pairs. + A new agent will be created with the parameters set to the values from the update_dict. + """ + new_agent = copy.deepcopy(agent) #.copy() # create a copy of the agent + set_module_parameters(new_agent, update_dict) # set the parameters of the new agent + return new_agent # return the new agent \ No newline at end of file diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 9e1037a2..3ffeb689 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -1,15 +1,9 @@ import numpy as np import copy -import heapq from dataclasses import dataclass from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace -from opto.trace.nodes import ParameterNode -from opto.trainer.utils import async_run, batch_run -from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify -from opto.trainer.evaluators import evaluate -from opto.trainer.loader import DataLoader +from opto.trainer.utils import batch_run from opto.trainer.guide import AutoGuide @dataclass diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index bf331e40..7c073156 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -1,8 +1,8 @@ from opto import trace from opto.trainer.loader import DataLoader from opto.trainer.sampler import Sampler -from opto.trainer.algorithms.search_algorithms import PrioritySearch as _PrioritySearch -from opto.trainer.algorithms.search_algorithms import ModuleCandidate +from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch as _PrioritySearch +from opto.trainer.algorithms.priority_search.priority_search import ModuleCandidate from opto.optimizers import OptoPrimeV2 from opto.trainer.guide import AutoGuide from opto.utils.llm import DummyLLM @@ -46,7 +46,7 @@ def forward(self, x): infos = [1, 2, 3, 4, 5] batch_size = 3 sub_batch_size = 2 -num_threads = 1 # 2 +num_threads = 2 # 2 dataset = {'inputs': xs, 'infos': infos} loader = DataLoader(dataset, batch_size=batch_size, randomize=False) sampler = Sampler(loader=loader, guide=Guide(), sub_batch_size=sub_batch_size, num_threads=num_threads) From ba386081220e3aa341330c6deb42f167a33053bd Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 21:37:52 +0000 Subject: [PATCH 134/172] Add missing search template --- .../priority_search/search_template.py | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 opto/trainer/algorithms/priority_search/search_template.py diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/trainer/algorithms/priority_search/search_template.py new file mode 100644 index 00000000..b5d2cb46 --- /dev/null +++ b/opto/trainer/algorithms/priority_search/search_template.py @@ -0,0 +1,221 @@ +import numpy as np +from typing import Union, List, Tuple, Dict, Any, Optional +from opto import trace +from opto.trainer.algorithms.basic_algorithms import Minibatch +from opto.trainer.loader import DataLoader +from opto.trainer.sampler import Sampler, RolloutsGraph + +# TODO save and load SearchTemplate +# TODO async version??? +# TODO create SYNC and ASYNC versions of the base class; add an attribute to the class to indicate + + +class Samples: + """ A container for samples collected during the search algorithm. It contains a list of RolloutsGraph objects + and a dataset with inputs and infos which created the list of RolloutsGraph. """ + + samples: List[RolloutsGraph] + dataset: Dict[str, List[Any]] # contains 'inputs' and 'infos' keys + + def __init__(self, samples: List[RolloutsGraph], dataset: Dict[str, List[Any]]): + assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." + assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + assert isinstance(dataset, dict), "dataset must be a dict." + assert 'inputs' in dataset and 'infos' in dataset, "dataset must contain 'inputs' and 'infos' keys." + + self.samples = samples + self.dataset = dataset # NOTE this cannot be extracted from the samples in general? + + def add_samples(self, samples): + """ Add samples to the Samples object. """ + assert isinstance(samples, Samples), "samples must be an instance of Samples." + samples = samples.samples # extract the samples from the Samples object + assert isinstance(samples, list), "samples must be a list of RolloutsGraph objects." + assert all(isinstance(s, RolloutsGraph) for s in samples), "All samples must be RolloutsGraph objects." + + # TODO assert xs and infos are in self.minibatch + # add a function to extract unique inputs and infos from the samples + + self.samples.extend(samples) + + def get_batch(self): + return self.dataset #['inputs'], self.minibatch['infos'] + + def __iter__(self): + """ Iterate over the samples. """ + return iter(self.samples) + + def __len__(self): + return sum(len(s) for s in self.samples) + + + +class SearchTemplate(Minibatch): + # This only uses __init__ and evaluate of Minibatch class. + """ This implements a generic template for search algorithm. """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size for broadcasting the agents + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent; if None, use train_dataset + test_guide = None, # guide to provide scores for the test set; if None, use guide + eval_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + **kwargs + ): + + ## Setup + test_frequency = eval_frequency # use eval_frequency as test_frequency # NOTE legacy notation + log_frequency = log_frequency or test_frequency # frequency of logging (default to test_frequency) + self.num_threads = num_threads or self.num_threads # Use provided num_threads or fall back to self.num_threads + test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided + test_guide = test_guide or guide + self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input + self.score_range = score_range or (-np.inf, np.inf) + + self.train_sampler = Sampler( + DataLoader(train_dataset, batch_size=batch_size), + guide, + num_threads=self.num_threads, + sub_batch_size=sub_batch_size, + score_range=self.score_range + ) + self._validate_dataset = validate_dataset # if None, the current batch will be used for validation + self.validate_sampler = Sampler( + DataLoader(validate_dataset if validate_dataset else {'inputs':[],'infos':[]}, batch_size=batch_size), + validate_guide or guide, + num_threads=self.num_threads, + sub_batch_size=None, # no sub-batch size for validation + score_range=self.score_range + ) + + # Evaluate the agent before learning + # NOTE set test_frequency < 0 to skip first evaluation + if (test_frequency is not None) and test_frequency > 0: + info_test = self.test(test_dataset, test_guide) # test self.agent + self.log(info_test) + + # Save the agent before learning if save_frequency > 0 + if (save_frequency is not None) and save_frequency > 0: + self.save(save_path) + + samples = None + self.n_epochs = 0 # number of epochs (full passes over the dataset) performed by the algorithm (This is incremented in sample) + self.n_samples = 0 # number of training samples processed by the algorithm (This is incremented in sample) + train_scores = [] # to store the scores of the agent during training + + while self.n_epochs < num_epochs : + + print(f"Epoch: {self.n_epochs}. Iteration: {self.n_iters}") + + # 1. Propose new parameters given the current state of the algorithm + # proposals: list of trace.Modules + update_dict, proposals, info_update = self.update(samples, verbose=verbose, **kwargs) + self.optimizer.update(update_dict) # update self.agent with the proposed parameters + + # 2. Get feedback on the proposed parameters on the current batch + # samples: Samples object containing the samples and the minibatch + samples, info_sample = self.sample(proposals, verbose=verbose, **kwargs) + + # Evaluate the agent after update + if (test_frequency is not None) and (self.n_iters % test_frequency == 0): + info_test = self.test(test_dataset, test_guide) # test self.agent + self.log(info_test, prefix="Test: ") + + # Save the algorithm state + if (save_frequency is not None and save_frequency > 0) and self.n_iters % save_frequency == 0: + self.save(save_path) + + # Log information + assert 'mean_score' in info_sample, "info_sample must contain 'mean_score'." + assert 'n_epochs' in info_sample, "info_sample must contain 'n_epochs'." + + train_scores.append(info_sample['mean_score']) # so that mean can be computed + if self.n_iters % log_frequency == 0: + self.logger.log('Average train score', np.mean(train_scores), self.n_iters, color='blue') + self.log(info_update, prefix="Update: ") + self.log(info_sample, prefix="Sample: ") + self.n_samples += len(samples) # update the number of samples processed + self.logger.log('Number of samples', self.n_samples, self.n_iters, color='blue') + # Log parameters + for p in self.agent.parameters(): + self.logger.log(f"Parameter: {p.name}", p.data, self.n_iters, color='red') + + # Update counters + self.n_epochs = info_sample['n_epochs'] # update the number of epochs completed + self.n_iters += 1 + return + + # Can be overridden by subclasses to implement specific sampling strategies + def sample(self, agents, verbose=False, **kwargs): + """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. + + Args: + agents (list): A list of trace.Modules (proposed parameters) to evaluate. + **kwargs: Additional keyword arguments that may be used by the implementation. + """ + samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch + + # Log information about the sampling + scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph + scores = [item for sublist in scores for item in sublist] # flatten the list of scores + log_info = { + 'mean_score': np.mean(scores), + 'n_epochs': self.train_sampler.n_epochs, + } + return samples, log_info + + def log(self, info_log, prefix=""): + """ Log the information from the algorithm. """ + for key, value in info_log.items(): + try: + if value is not None: + self.logger.log(f"{prefix}{key}", value, self.n_iters) + except Exception as e: + print(e) + + def test(self, test_dataset, guide): + min_score = self.score_range[0] + # Test the agent's performance + test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], + min_score=min_score, num_threads=self.num_threads, + description=f"Evaluating agent (iteration {self.n_iters})") # and log + return {'test_score': test_score} + + def save(self, save_path): + self.save_agent(save_path, self.n_iters) + # TODO save full state of self + + # Unimplemented methods that should be implemented by subclasses + def update(self, samples=None, verbose=False, **kwargs): + """ Update the agent based on the provided samples. + Args: + samples (list): A list of samples from the previous iteration. If None, the agent's parameters are returned without updating. + verbose (bool, optional): Whether to print verbose output. Defaults to False. + **kwargs: Additional keyword arguments that may be used by the implementation. + Returns: + update_dict (dict of Parameter: Any): A dictionary containing the updated parameters of the agent. + proposals (list of trace.Module): A list of proposed parameters (trace.Module) after the update. + info_log (dict of str: Any): A dictionary containing logging information about the update process. + + This method updates the agent's parameters based on samples of the training dataset and validation dataset (provided by self.get_validate_dataset). + In addition, it return new agents (proposals) that can be used for collecting data for the next iteration. + """ + raise NotImplementedError("The update method should be implemented by subclasses.") + # return update_dict, proposals, info_log From c67a202cb56b633735c4fdd33dae0d9885db2761 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 22:52:32 +0000 Subject: [PATCH 135/172] Add print statements --- .../algorithms/priority_search/priority_search.py | 15 ++++++++++----- .../algorithms/priority_search/search_template.py | 6 +++++- opto/trainer/sampler.py | 1 + tests/unit_tests/test_sampler.py | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index 10da4b89..f1c53b64 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -235,7 +235,7 @@ def propose(self, samples, verbose=False, **kwargs): Returns: candidates (list of ModuleCandidate): A list of proposed candidates for the next iteration. """ - + print("--- Proposing new parameters...") if verbose else None assert isinstance(samples, Samples), "samples must be an instance of Samples." samples = samples.samples # list of RolloutsGraph objects n_proposals = self.num_proposals # number of proposals to generate per optimizer @@ -276,10 +276,11 @@ def _step(optimizer): return update_dict # return the proposed parameters args_list = [(o,) for o in optimizers ] * n_proposals # repeat args_list n_proposals times + assert len(args_list) == n_subgraphs * n_proposals, "args_list must have length n_subgraphs * n_proposals" update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, max_workers=self.num_threads, # use the number of threads specified in the class - description="Running optimizers on samples") + description=f"Running optimizers to generate {n_proposals} proposals for each of {n_subgraphs} sub batches",) # update_dicts is a list of dicts of length n_agents * n_proposals # Create ModuleCandidate objects for each proposed update_dict @@ -296,6 +297,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): Returns: results (dict): A dictionary where the keys are ids of ModuleCandidate objects and the values are ModuleCandidate and lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. """ + print("--- Validating candidates...") if verbose else None # Get the validation dataset from the samples. If no validation dataset is provided, use the current batch. if self._validate_dataset is None: @@ -358,19 +360,20 @@ def validate(self, candidates, samples, verbose=False, **kwargs): - def update_memory(self, validate_results, **kwargs): + def update_memory(self, validate_results, verbose: bool = False, **kwargs): """ Update the priority queue with the validation results. Args: validate_results (dict): A dictionary where the keys are ModuleCandidate objects and the values are lists of rollouts (list of dicts) containing the module, x, info, target, score, feedback. **kwargs: Additional keyword arguments that may be used by the implementation. """ + print("--- Updating memory with validation results...") if verbose else None for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate score = self.compute_score(candidate) # compute the score for the candidate self.memory.push(score, candidate) #### - def explore(self, **kwargs): + def explore(self, verbose: bool = False, **kwargs): """ Explore the parameter space and propose new candidates. Args: **kwargs: Additional keyword arguments that may be used by the implementation. @@ -378,6 +381,7 @@ def explore(self, **kwargs): list: A list of proposed candidates. dict: A dictionary containing logging information about the exploration. """ + print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None # pop top self.num_candidates candidates from the priority queue top_candidates = [] while len(top_candidates) < self.num_candidates and self.memory: @@ -386,7 +390,7 @@ def explore(self, **kwargs): return top_candidates, {} - def exploit(self, **kwargs): + def exploit(self, verbose: bool = False, **kwargs): # NOTE This function can be overridden by subclasses to compute a different score """ Exploit the best candidate from the priority queue. This method should not change the priority queue. Args: @@ -394,6 +398,7 @@ def exploit(self, **kwargs): Returns: ModuleCandidate: The best candidate from the priority queue. """ + print("--- Exploiting the best candidate...") if verbose else None # Right now, we just return the best candidate from the priority queue # This function can be overridden by subclasses to implement a different exploitation strategy if not self.memory: diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/trainer/algorithms/priority_search/search_template.py index b5d2cb46..bb6b2228 100644 --- a/opto/trainer/algorithms/priority_search/search_template.py +++ b/opto/trainer/algorithms/priority_search/search_template.py @@ -48,6 +48,11 @@ def __iter__(self): def __len__(self): return sum(len(s) for s in self.samples) + @property + def n_sub_batches(self) -> int: + """ Number of sub-batches in the samples. """ + return len(self.samples) + class SearchTemplate(Minibatch): @@ -171,7 +176,6 @@ def sample(self, agents, verbose=False, **kwargs): **kwargs: Additional keyword arguments that may be used by the implementation. """ samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch - # Log information about the sampling scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph scores = [item for sublist in scores for item in sublist] # flatten the list of scores diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 3ffeb689..7cc50412 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -310,4 +310,5 @@ def sample(self, agents): min_score=self.score_range[0], description=description) + assert len(samples) == len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0)), f"Expected {len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0))} samples, got {len(samples)}" return samples, batch diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index d6fd6d16..fd9ceca4 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -2,7 +2,7 @@ from opto.trainer.sampler import Sampler from opto.trainer.loader import DataLoader from opto.trainer.guide import AutoGuide -from opto.trainer.algorithms.search_algorithms import is_node_copy +from opto.trainer.algorithms.priority_search.utils import is_node_copy class Guide(AutoGuide): From 70cba726f266025dc2c44d4e581e634b3e69e772 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 23:08:16 +0000 Subject: [PATCH 136/172] Update printing --- opto/trainer/algorithms/priority_search/priority_search.py | 6 +++--- opto/trainer/algorithms/priority_search/search_template.py | 4 ++-- opto/trainer/sampler.py | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index f1c53b64..39768921 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -280,7 +280,7 @@ def _step(optimizer): update_dicts = async_run([_step]*n_subgraphs*n_proposals, # run the optimizer step for each agent in parallel args_list=args_list, max_workers=self.num_threads, # use the number of threads specified in the class - description=f"Running optimizers to generate {n_proposals} proposals for each of {n_subgraphs} sub batches",) + description=f"Calling optimizers: Generating {n_proposals} proposals for each of {n_subgraphs} sub batches",) # update_dicts is a list of dicts of length n_agents * n_proposals # Create ModuleCandidate objects for each proposed update_dict @@ -307,7 +307,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): self.validate_sampler.batch_size = len(validate_dataset['inputs']) # set the batch size to the number of inputs in the validation dataset candidate_agents = [c.get_module() for c in candidates] # get the modules from the candidates - validate_samples = Samples(*self.validate_sampler.sample(candidate_agents)) # list of RolloutsGraph objects + validate_samples = Samples(*self.validate_sampler.sample(candidate_agents, description_prefix='Validating newly proposed candidates: ')) # list of RolloutsGraph objects exploration_candidates = self._exploration_candidates # exploration candidates from the previous iteration @@ -319,7 +319,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): else: # validate the agents in the validate_dataset # exploration_agents = [rollouts.module for rollouts in samples.samples] # NOTE this might contain some duplicates due to sub_batch_size < batch_size exploitation_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates - exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents)) # sample the exploration agents + exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, description_prefix='Validating exploration candidates: ')) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/trainer/algorithms/priority_search/search_template.py index bb6b2228..e37f8cff 100644 --- a/opto/trainer/algorithms/priority_search/search_template.py +++ b/opto/trainer/algorithms/priority_search/search_template.py @@ -175,7 +175,7 @@ def sample(self, agents, verbose=False, **kwargs): agents (list): A list of trace.Modules (proposed parameters) to evaluate. **kwargs: Additional keyword arguments that may be used by the implementation. """ - samples = Samples(*self.train_sampler.sample(agents)) # create a Samples object to store the samples and the minibatch + samples = Samples(*self.train_sampler.sample(agents, description_prefix='Sampling training minibatch: ')) # create a Samples object to store the samples and the minibatch # Log information about the sampling scores = [ g.get_scores() for g in samples.samples] # list of list of scores for each RolloutsGraph scores = [item for sublist in scores for item in sublist] # flatten the list of scores @@ -199,7 +199,7 @@ def test(self, test_dataset, guide): # Test the agent's performance test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=self.num_threads, - description=f"Evaluating agent (iteration {self.n_iters})") # and log + description=f"Evaluating agent") # and log return {'test_score': test_score} def save(self, save_path): diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 7cc50412..4928a390 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -234,12 +234,11 @@ def n_epochs(self): """ Get the number of epochs of the loader. """ return self.loader.n_epochs - def sample(self, agents): + def sample(self, agents, description_prefix=''): """ Sample a batch of data from the loader and evaluate the agents. Args: agents (list): A list of trace.Modules (proposed parameters) to evaluate. - **kwargs: Additional keyword arguments that may be used by the implementation. Returns: batch (dict): @@ -303,7 +302,7 @@ def sample(self, agents): configs.append(RolloutConfig(module=agent, xs=_xs, infos=_infos, guide=self.guide)) # Sample rollouts using the configs - description = f"Sampling {len(agents)} agents on {batch_size} inputs" + description = description_prefix + f"Sampling {len(agents)} agents on {batch_size} inputs" samples = sample_rollouts(configs, forward=self.forward, num_threads=self.num_threads, From 57a740b763fd042a8f1d4e4d54db0a609a926966 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 23:10:05 +0000 Subject: [PATCH 137/172] Update to use pytest --- tests/unit_tests/test_priority_search.py | 59 +++++++++++++----------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 7c073156..47d90fb1 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -62,7 +62,7 @@ class PrioritySearch(_PrioritySearch): # This class is for testing the PrioritySearch algorithm def propose(self, samples, verbose=False, n_proposals=1, **kwargs): - print("Propose at iteration:", self.n_iters) + print("[UnitTest] Propose at iteration:", self.n_iters) # assert len(samples) == batch_size, f"Expected {batch_size} samples, got {len(samples)}" # assert len(samples) == len(agents) * np.ceil(batch_size / self.sub_batch_size), f"Expected {len(agents) * np.ceil(batch_size / self.sub_batch_size)} samples, got {len(samples)}" @@ -74,7 +74,7 @@ def propose(self, samples, verbose=False, n_proposals=1, **kwargs): return candidates def validate(self, candidates, samples, verbose=False, **kwargs): - print("Validate at iteration:", self.n_iters) + print("[UnitTest] Validate at iteration:", self.n_iters) assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" validate_results = super().validate(candidates, samples, verbose=verbose, **kwargs) @@ -87,15 +87,14 @@ def validate(self, candidates, samples, verbose=False, **kwargs): return validate_results def exploit(self, **kwargs): - print("Exploit at iteration:", self.n_iters) - + print("[UnitTest] Exploit at iteration:", self.n_iters) candidate, info_dict = super().exploit(**kwargs) assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" assert isinstance(info_dict, dict), "Expected info_dict to be a dictionary" return candidate, info_dict def explore(self, **kwargs): - print("Explore at iteration:", self.n_iters) + print("[UnitTest] Explore at iteration:", self.n_iters) candidates, info_dict = super().explore(**kwargs) assert isinstance(candidates, list) @@ -107,7 +106,6 @@ def explore(self, **kwargs): num_candidates = min(self.num_candidates, 2) # in this example, memory will contain at most 2 unique candidates assert len(candidates) == num_candidates, f"Expected {num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" - return candidates, info_dict @@ -133,26 +131,31 @@ def _llm_callable(messages, **kwargs): """ -dummy_llm = DummyLLM(_llm_callable) -agent = Agent() -optimizer = OptoPrimeV2( - agent.parameters(), +def test_priority_search(): + """ + Test the PrioritySearch algorithm with a dummy LLM and a simple agent. + """ + # Create a dummy LLM and an agent + dummy_llm = DummyLLM(_llm_callable) + agent = Agent() + optimizer = OptoPrimeV2( + agent.parameters(), llm=dummy_llm, -) - -algo = PrioritySearch( - agent, - optimizer, -) - -algo.train( - guide=Guide(), - train_dataset=dataset, - batch_size=batch_size, - sub_batch_size=sub_batch_size, - num_threads=num_threads, - num_candidates=num_candidates, - num_proposals=num_proposals, - memory_size=memory_size, - verbose=False, -) + ) + + algo = PrioritySearch( + agent, + optimizer, + ) + + algo.train( + guide=Guide(), + train_dataset=dataset, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + num_threads=num_threads, + num_candidates=num_candidates, + num_proposals=num_proposals, + memory_size=memory_size, + verbose=False, #'output', + ) From 21ca662ed76c6c7427a4f10aaf2035bb263c4615 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 23:10:44 +0000 Subject: [PATCH 138/172] Rename example --- ...k_search_algo.py => priority_search_example.py} | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) rename examples/{gsm8k_search_algo.py => priority_search_example.py} (87%) diff --git a/examples/gsm8k_search_algo.py b/examples/priority_search_example.py similarity index 87% rename from examples/gsm8k_search_algo.py rename to examples/priority_search_example.py index 3e21f7cf..7928a3d3 100644 --- a/examples/gsm8k_search_algo.py +++ b/examples/priority_search_example.py @@ -54,11 +54,14 @@ def main(): # set seed seed = 42 num_epochs = 1 - batch_size = 3 - sub_batch_size = 2 + batch_size = 3 # number of queries to sample from the training data + sub_batch_size = 2 # number of queries each optimizer sees + num_proposals = 3 # number of proposals to generate for each query + num_candidates = 2 # number of candidates for exploration score_range = (0, 1) # range of the score for the guide eval_frequency = -1 num_eval_samples = 2 + num_threads = 10 datasize = 5 verbose = True @@ -66,12 +69,13 @@ def main(): student_model = None # use default model optimizer_model = None # use default model + np.random.seed(seed) # In this example, we use the GSM8K dataset, which is a dataset of math word problems. # We will look the training error of the agent on a small portion of this dataset. - train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:datasize] - train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer']) + train_dataset = datasets.load_dataset('BBEH/bbeh')['train'][:datasize] + train_dataset = dict(inputs=train_dataset['input'], infos=train_dataset['target']) test_dataset = train_dataset agent = Learner(llm=LLM(student_model)) @@ -93,6 +97,8 @@ def main(): test_dataset=test_dataset, num_threads=num_threads, sub_batch_size=sub_batch_size, + num_proposals=num_proposals, + num_candidates=num_candidates, score_range=score_range, num_eval_samples=num_eval_samples, verbose='output' if verbose else False) From 87b315692abc2b44e4ad650d80b1e553e730f367 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 24 Jul 2025 23:23:33 +0000 Subject: [PATCH 139/172] Add docstring --- .../priority_search/priority_search.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index 39768921..d63d8b6f 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -11,7 +11,6 @@ from opto.trainer.algorithms.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict -# TODO make this hashable? class ModuleCandidate: """ A container used by PrioritySearch to store a candidate module as (its base module and update dictionary) and its statistics. """ @@ -138,7 +137,21 @@ def best(self): class PrioritySearch(SearchTemplate): - """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. """ + """ A search algorithm that uses a priority queue to explore the parameter space and propose new candidates. + + It provides a scalable template for implementing search algorithms based on asynchronous generation, validation, and testing. + In each iteration, + 1. It proposes a best agent and a set of `num_candidates` exploration agents that have the highest scores in the priority queue. + 2. The best agent is tested for performance if eval_frequency is met. + 3. A minibatch of `batch_size` samples are drawn from the training dataset, and the exploration agents are run on the samples. This creates a set of agent rollouts, where each rollout contains the agent module, input, info, target, score, and feedback. For each agent, rollouts of size `sub_batch_size` are grouped together as a connected subgraph (represented as the RolloutsGraph object). In total, this step creates `num_subgraphs = num_candidates * ceil(batch_size / sub_batch_size)` subgraphs. + 4. Optimizer is run on each subgraph to propose new parameters for the agents. `num_proposals` proposals are generated for each subgraph. This results in `num_subgraphs * num_proposals` total proposals. + 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_proposals is set to True, the exploration candidates are also validated. + 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). + + This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_score` methods. + The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and + the `compute_score` method is used to compute the score for ranking in the priority queue. + """ def train(self, guide, # guide to provide feedback From d8305b3b85d8c033af1e1247b780c4b5d39acb4d Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 25 Jul 2025 00:26:53 +0000 Subject: [PATCH 140/172] Add examples of priority search. --- .../algorithms/priority_search/__init__.py | 3 +- .../algorithms/priority_search/examples.py | 214 ++++++++++++++++++ .../priority_search/priority_search.py | 4 +- opto/trainer/sampler.py | 1 + 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 opto/trainer/algorithms/priority_search/examples.py diff --git a/opto/trainer/algorithms/priority_search/__init__.py b/opto/trainer/algorithms/priority_search/__init__.py index 68fe26c0..caaf664f 100644 --- a/opto/trainer/algorithms/priority_search/__init__.py +++ b/opto/trainer/algorithms/priority_search/__init__.py @@ -1 +1,2 @@ -from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch \ No newline at end of file +from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch +from opto.trainer.algorithms.priority_search.examples import SequentialUpdate, SequentialSearch, BeamSearch \ No newline at end of file diff --git a/opto/trainer/algorithms/priority_search/examples.py b/opto/trainer/algorithms/priority_search/examples.py new file mode 100644 index 00000000..90f6cb14 --- /dev/null +++ b/opto/trainer/algorithms/priority_search/examples.py @@ -0,0 +1,214 @@ + +from opto.trainer.algorithms.priority_search import PrioritySearch +from typing import Union, Optional + +# Below we define several algorithms that use the PrioritySearch class. + + +class SequentialUpdate(PrioritySearch): + """ A basic algorithm that explores the parameter space and proposes new candidates one by one. + + This is realized by setting + + num_candidates = 1 + num_proposals = 1 + memory_size = 1 + + This is the same as MinibatchAlgorithm when + 1. no validation set is provided + 2. sub_batch_size is None or batch_size. + + validate_proposals here acts the same as `ensure_improvement` flag in MinibatchAlgorithm + """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size that each optimizer attends to + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer + default_score: float = float('inf'), # default score assigned to priority queue candidates + validate_proposals: bool = True, # whether to validate the proposed parameters + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + # Additional keyword arguments + **kwargs + ): + + num_candidates = 1 # SequentialSearch only proposes one candidate at a time + num_proposals = 1 # SequentialSearch only generates one proposal at a time + memory_size = 1 # SequentialSearch only stores one candidate at a time in the heap memory + # validate_proposals is the same as `ensure_improvement` flag in MinibatchAlgorithm + + return super().train(guide, train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + test_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + num_candidates=num_candidates, + num_proposals=num_proposals, + default_score=default_score, + validate_proposals=validate_proposals, + memory_size=memory_size, **kwargs) + + +class SequentialSearch(PrioritySearch): + """ A sequential search that generates one candidate in each iteration by validating multiple proposals. + + This is realized by setting + num_proposals = 1 + memory_size = 1 + + This is the same as BasicSearchAlgorithm when + 1. a validation set is provided + 2. validate_proposals is True. + 3. sub_batch_size is None or batch_size. + """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size that each optimizer attends to + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer + default_score: float = float('inf'), # default score assigned to priority queue candidates + validate_proposals: bool = True, # whether to validate the proposed parameters + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + # Additional keyword arguments + **kwargs + ): + + num_candidates = 1 # SequentialSearch only generates one candidate at a time + memory_size = 1 # MultiSequentialUpdate only stores one candidate at a time in the heap memory + # validate_proposals is the same as `ensure_improvement` flag in MinibatchAlgorithm + + return super().train(guide, train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + test_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + num_candidates=num_candidates, + num_proposals=num_proposals, + default_score=default_score, + validate_proposals=validate_proposals, + memory_size=memory_size, **kwargs) + +class BeamSearch(PrioritySearch): + """ A beam search algorithm that explores the parameter space and proposes new candidates based on the best candidates in the priority queue. + + This is realized by setting + num_proposals = beam_size + memory_size = beam_size + + """ + + def train(self, + guide, # guide to provide feedback + train_dataset, # dataset of (x, info) pairs to train the agent + *, + # validation + validate_dataset = None, # same format as train_dataset; if None use the current batch. + validate_guide = None, # to provide scores for the validation set + # training loop + batch_size = 1, # batch size for updating the agent + sub_batch_size = None, # sub-batch size that each optimizer attends to + score_range = None, # minimum score to update the agent + num_epochs = 1, # number of training epochs + num_threads = None, # maximum number of threads to use + verbose = False, # whether to print the output of the agent + # evaluation + test_dataset = None, # dataset of (x, info) pairs to evaluate the agent + test_frequency: Union[int, None] = 1, # frequency of evaluation + num_eval_samples: int = 1, # number of samples to use to evaluate each input + # logging + log_frequency = None, # frequency of logging + save_frequency: Union[int, None] = None, # frequency of saving the agent + save_path: str = "checkpoints/agent.pkl", # path to save the agent + # Priority Search specific parameters + num_candidates: int = 10, # number of candidates to propose for exploration + num_proposals: int = 1, # number of proposals to generate per optimizer; this is beam_size in beam search. + default_score: float = float('inf'), # default score assigned to priority queue candidates + validate_proposals: bool = True, # whether to validate the proposed parameters + memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + **kwargs): + + # num_candidates acts as the beam size in beam search. + memory_size = num_candidates + + return super().train(guide, train_dataset, + validate_dataset=validate_dataset, + validate_guide=validate_guide, + batch_size=batch_size, + sub_batch_size=sub_batch_size, + score_range=score_range, + num_epochs=num_epochs, + num_threads=num_threads, + verbose=verbose, + test_dataset=test_dataset, + test_frequency=test_frequency, + num_eval_samples=num_eval_samples, + log_frequency=log_frequency, + save_frequency=save_frequency, + save_path=save_path, + num_candidates=num_candidates, # beam size + num_proposals=num_proposals, # number of proposals to generate per optimizer + default_score=default_score, + validate_proposals=validate_proposals, + memory_size=memory_size, **kwargs) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index d63d8b6f..8d267bf4 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -151,6 +151,8 @@ class PrioritySearch(SearchTemplate): This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_score` methods. The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and the `compute_score` method is used to compute the score for ranking in the priority queue. + + By default, `compute_score` computes the mean score of the rollouts. `exploit` simply returns the best candidate from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. """ def train(self, @@ -442,4 +444,4 @@ def compute_score(self, candidate): scores = [r['score'] for r in candidate.rollouts] default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates - return np.mean(scores) if scores else self.default_score + return np.mean(scores) if scores else self.default_score \ No newline at end of file diff --git a/opto/trainer/sampler.py b/opto/trainer/sampler.py index 4928a390..3d46ea05 100644 --- a/opto/trainer/sampler.py +++ b/opto/trainer/sampler.py @@ -310,4 +310,5 @@ def sample(self, agents, description_prefix=''): description=description) assert len(samples) == len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0)), f"Expected {len(agents)*(batch_size // self.sub_batch_size + (1 if batch_size % self.sub_batch_size > 0 else 0))} samples, got {len(samples)}" + return samples, batch From 368c13149aa8cc046f25e661d320fab31e92159d Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 25 Jul 2025 02:45:15 +0000 Subject: [PATCH 141/172] Fix a bug in of deleting keys in default_json_keys --- opto/optimizers/optoprime.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6cbca909..fe7a6e49 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -258,7 +258,7 @@ class OptoPrime(Optimizer): final_prompt_with_variables = dedent( """ What are your suggestions on variables {names}? - + Your response: """ ) @@ -333,13 +333,14 @@ def __init__( if prompt_symbols is not None: self.prompt_symbols.update(prompt_symbols) if json_keys is not None: - self.default_json_keys.update(json_keys) - if self.default_json_keys['answer'] is None: # answer field is not needed - del self.default_json_keys['answer'] - if 'answer' not in self.default_json_keys: + self.default_json_keys.update(json_keys) + # if self.default_json_keys['answer'] is None: + # del self.default_json_keys['answer'] + # NOTE del cause KeyError if the key is not in the dict due to changing class attribute + if 'answer' not in self.default_json_keys or self.default_json_keys['answer'] is None: # answer field is not needed # If 'answer' is not in the json keys, we use the no-answer format self.output_format_prompt = self.output_format_prompt_no_answer.format(**self.default_json_keys) - else: # If 'answer' is in the json keys, we use the original format of OptoPrime + else: # If 'answer' is in the json keys, we use the original format of OptoPrime self.output_format_prompt = self.output_format_prompt_original.format(**self.default_json_keys) self.use_json_object_format = use_json_object_format self.highlight_variables = highlight_variables @@ -450,8 +451,8 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): ) + user_prompt ) - - + + if self.highlight_variables: var_names = [] for k, v in summary.variables.items(): @@ -618,13 +619,13 @@ def call_llm( {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] - + response_format = {"type": "json_object"} if self.use_json_object_format else None try: # Try tp force it to be a json object response = self.llm(messages=messages, max_tokens=max_tokens, response_format=response_format) except Exception: response = self.llm(messages=messages, max_tokens=max_tokens) - + response = response.choices[0].message.content if verbose: From 7364b50c5dcb0bf4eda6dd433410f29be54e78dd Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 25 Jul 2025 21:03:11 +0000 Subject: [PATCH 142/172] Add use_best_candidate_to_explore flag (True as default). --- .../priority_search/priority_search.py | 21 ++++++++++-------- tests/unit_tests/test_priority_search.py | 22 ++++++++++++------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index 8d267bf4..154494a9 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -181,7 +181,8 @@ def train(self, num_candidates: int = 10, # number of candidates to propose for exploration num_proposals: int = 1, # number of proposals to generate per optimizer default_score: float = float('inf'), # default score assigned to priority queue candidates - validate_proposals: bool = True, # whether to validate the proposed parameters + validate_proposals: bool = True, # whether to validate the proposed parameters for exploration + use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set # Additional keyword arguments **kwargs @@ -191,6 +192,7 @@ def train(self, self.num_candidates = num_candidates # number of candidates to propose by each optimizer call self.num_proposals = num_proposals self.validate_proposals = validate_proposals # whether to validate the proposed parameters + self.use_best_candidate_to_explore = use_best_candidate_to_explore self.default_score = default_score self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit self.memory.push(self.default_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate @@ -224,19 +226,18 @@ def update(self, samples=None, verbose=False, **kwargs): # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information # 4. Explore and exploit the priority queue - best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue - exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates + self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue + self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates - self._exploration_candidates = exploration_candidates # TODO Log information about the update info_log = { - 'best_candidate_score': best_candidate.score(), - 'num_exploration_candidates': len(exploration_candidates), + 'best_candidate_score': self._best_candidate.score(), + 'num_exploration_candidates': len(self._exploration_candidates), } info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step - return best_candidate.update_dict, [c.get_module() for c in exploration_candidates], info_log + return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log def propose(self, samples, verbose=False, **kwargs): """ Analyzing samples and propose new parameters using self.optimizer. An independent optimizer is used for the minibatch generated by one agent and generates n_proposals proposals. @@ -374,7 +375,6 @@ def validate(self, candidates, samples, verbose=False, **kwargs): return results - def update_memory(self, validate_results, verbose: bool = False, **kwargs): """ Update the priority queue with the validation results. Args: @@ -398,9 +398,12 @@ def explore(self, verbose: bool = False, **kwargs): """ print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None # pop top self.num_candidates candidates from the priority queue - top_candidates = [] + top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] while len(top_candidates) < self.num_candidates and self.memory: score, candidate = self.memory.pop() # pop the top candidate from the priority queue + if self.use_best_candidate_to_explore: + if candidate == self._best_candidate: # skip if it is already in the top candidates + continue top_candidates.append(candidate) # add the candidate to the top candidates return top_candidates, {} diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 47d90fb1..c1bf703b 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -9,6 +9,7 @@ import re import numpy as np +import copy class Guide(AutoGuide): @@ -63,26 +64,23 @@ class PrioritySearch(_PrioritySearch): def propose(self, samples, verbose=False, n_proposals=1, **kwargs): print("[UnitTest] Propose at iteration:", self.n_iters) - # assert len(samples) == batch_size, f"Expected {batch_size} samples, got {len(samples)}" - # assert len(samples) == len(agents) * np.ceil(batch_size / self.sub_batch_size), f"Expected {len(agents) * np.ceil(batch_size / self.sub_batch_size)} samples, got {len(samples)}" candidates = super().propose(samples, verbose=verbose, n_proposals=n_proposals, **kwargs) # In this example this will always be value 5 assert isinstance(candidates, list), "Expected candidates to be a list" assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" - assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" + assert len(candidates) == samples.n_sub_batches * self.num_proposals, f"Expected {samples.n_sub_batches * self.num_proposals} candidates, got {len(candidates)}" return candidates def validate(self, candidates, samples, verbose=False, **kwargs): print("[UnitTest] Validate at iteration:", self.n_iters) - assert len(candidates) == np.ceil(batch_size / sub_batch_size) * self.num_proposals, f"Expected {np.ceil(batch_size / sub_batch_size) * self.num_proposals} candidates, got {len(candidates)}" validate_results = super().validate(candidates, samples, verbose=verbose, **kwargs) assert isinstance(validate_results, dict), "Expected validate_results to be a dict" assert all(isinstance(v, ModuleCandidate) for v in validate_results.keys()), "All keys should be ModuleCandidate instances" keys = list(validate_results.keys()) # should contain one from exploration and one from exploitation - assert len(validate_results) == 2, "In this example, all proposals are the same, so we expect only two validate results." + # assert len(validate_results) == 2, "In this example, all proposals are the same, so we expect only two validate results." return validate_results @@ -91,6 +89,14 @@ def exploit(self, **kwargs): candidate, info_dict = super().exploit(**kwargs) assert isinstance(candidate, ModuleCandidate), "Expected candidate to be an instance of ModuleCandidate" assert isinstance(info_dict, dict), "Expected info_dict to be a dictionary" + + # XXX Here we simulate a different best candidate is given + assert self.use_best_candidate_to_explore, "Expected use_best_candidate_to_explore to be True in this unit test" + candidate = copy.deepcopy(candidate) # Ensure we return a copy + for p in candidate.base_module.parameters(): + candidate.update_dict[p] = p.data + 100 + # This will be different the exploration candidates + return candidate, info_dict def explore(self, **kwargs): @@ -101,10 +107,10 @@ def explore(self, **kwargs): assert isinstance(info_dict, dict) if self.n_iters == 0: - assert len(candidates) == 1, f"Expected 1 candidate, got {len(candidates)}" + assert len(candidates) == 2, f"Expected 2 candidates, got {len(candidates)}" + # one from the init parameter and one from the hacked best candidate else: - num_candidates = min(self.num_candidates, 2) # in this example, memory will contain at most 2 unique candidates - assert len(candidates) == num_candidates, f"Expected {num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" + assert len(candidates) <= self.num_candidates, f"Expect no more than {self.num_candidates} candidates at iter {self.n_iters}, got {len(candidates)}" assert all(isinstance(c, ModuleCandidate) for c in candidates), "All candidates should be ModuleCandidate instances" return candidates, info_dict From e6ab7ed63941ffabb9c90465df6e003b09a669fb Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 25 Jul 2025 22:29:14 +0000 Subject: [PATCH 143/172] Add UCB score, update logging, and score_range attribute. --- examples/priority_search_example.py | 2 + .../priority_search/priority_search.py | 144 ++++++++++++++---- .../priority_search/search_template.py | 29 +++- 3 files changed, 145 insertions(+), 30 deletions(-) diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index 7928a3d3..4739ee0a 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -61,6 +61,7 @@ def main(): score_range = (0, 1) # range of the score for the guide eval_frequency = -1 num_eval_samples = 2 + score_function = 'mean' num_threads = 10 datasize = 5 @@ -101,6 +102,7 @@ def main(): num_candidates=num_candidates, score_range=score_range, num_eval_samples=num_eval_samples, + score_function=score_function, verbose='output' if verbose else False) diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/trainer/algorithms/priority_search/priority_search.py index 154494a9..0a9f4aca 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/trainer/algorithms/priority_search/priority_search.py @@ -30,6 +30,9 @@ def __init__(self, self.update_dict = remap_update_dict(self.base_module, self.update_dict) self.rollouts = [] # list of dicts containing the rollout information (not RolloutsGraph, but a list of dicts) self.created_time = time.time() + self._n_updates = 0 # number of times this candidate has been updated + self._n_confidence_queries = 1 # number of times the confidence score has been queried + self._confidence_interval = None def get_module(self): """ Apply the update_dict to the base_module and return the updated module. @@ -81,19 +84,78 @@ def add_rollouts(self, rollouts: List[Dict[str, Any]]): "Each rollout must contain 'module', 'x', 'info', 'target', 'score', and 'feedback' keys." self.rollouts.extend(rollouts) + self._confidence_interval = None # reset the confidence interval + self._n_updates += 1 # increment the number of updates - def score(self): + def mean_score(self): """ Compute the score of the candidate based on the rollouts. """ if not self.rollouts: return None scores = [r['score'] for r in self.rollouts] return np.mean(scores) if scores else None + def compute_score_confidence(self, min_score, max_score, scaling_constant=1.0): + """Compute the UCB, mean, LCB score for the candidate. After queried, the number of confidence queries is incremented. + + UCB = mean_score + scaling_constant * sqrt(ln(total_trials) / candidate_trials) * (max_score - min_score) + UCB = clip(UCB, min_score, max_score) + + LCB = mean_score - scaling_constant * sqrt(ln(total_trials) / candidate_trials) * (max_score - min_score) + LCB = clip(LCB, min_score, max_score) + + Args: + candidate (ModuleCandidate): The candidate for which to compute the UCB score. + Returns: + float: The computed UCB score for the candidate. + """ + # Get scores from rollouts + scores = [r['score'] for r in self.rollouts] + + # If no rollouts, return a high exploration score to encourage trying this candidate + if not scores: + return min_score, None, max_score + + # Calculate mean score for this candidate + mean_score = np.mean(scores) + candidate_trials = len(scores) + + # Calculate how many times the confidence interval has been used to form a union bound + total_trials = min(self._n_confidence_queries) + 1 # this is an upper bound, since log(1) = 0 + + # Compute the exploration term based on Hoeffding's inequality + exploration_term = scaling_constant * np.sqrt(np.log(total_trials) / candidate_trials) * (max_score - min_score) + + # Calculate UCB score + ucb_score = mean_score + exploration_term + ucb_score = np.clip(ucb_score, min_score, max_score) + + # Calculate LCB score + lcb_score = mean_score - exploration_term + lcb_score = np.clip(lcb_score, min_score, max_score) + + self._n_confidence_queries += 1 # increment the number of confidence queries + + self._confidence_interval = dict(lcb_score=lcb_score, ucb_score=ucb_score, mean_score=mean_score) + return lcb_score, mean_score, ucb_score + + @property + def confidence_interval(self): + # This is a cached property that returns the confidence interval of the candidate. + # This is for accessing the confidence interval without increasing the number of confidence queries. E.g. this is useful when using both LCB and UCB of the same candidate. + if self._confidence_interval is None: + raise ValueError("Confidence interval has not been computed yet. Call compute_score_confidence() first.") + return self._confidence_interval + @property def num_rollouts(self): """ Return the number of rollouts collected for this candidate. """ return len(self.rollouts) + @property + def n_updates(self): + """ Return the number of times this candidate has been updated. """ + return self._n_updates + class HeapMemory: # This is a basic implementation of a heap memory that uses a priority queue to store candidates. # Later on this will be replaced by a memory DB. @@ -148,11 +210,11 @@ class PrioritySearch(SearchTemplate): 5. The proposed parameters are validated by running the agents on the validation dataset, which can be the current batch or a separate validation dataset when provided. When validate_proposals is set to True, the exploration candidates are also validated. 6. The validation results are used to update the priority queue, which stores the candidates and their scores. The candidates are stored as ModuleCandidate objects, which contain the base module, update dictionary, and rollouts (i.e. raw statistics of the candidate). - This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_score` methods. + This algorithm template can be subclassed to implement specific search algorithms by overriding the `exploit`, `explore`, and `compute_priority` methods. The `exploit` method is used to select the best candidate from the priority queue, the `explore` method is used to generate new candidates from the priority queue, and - the `compute_score` method is used to compute the score for ranking in the priority queue. + the `compute_priority` method is used to compute the score for ranking in the priority queue. - By default, `compute_score` computes the mean score of the rollouts. `exploit` simply returns the best candidate from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. + By default, `compute_priority` computes the mean score of the rollouts. `exploit` simply returns the best candidate from the priority queue, and `explore` generates the top `num_candidates` candidates from the priority queue. """ def train(self, @@ -180,10 +242,11 @@ def train(self, # Priority Search specific parameters num_candidates: int = 10, # number of candidates to propose for exploration num_proposals: int = 1, # number of proposals to generate per optimizer - default_score: float = float('inf'), # default score assigned to priority queue candidates validate_proposals: bool = True, # whether to validate the proposed parameters for exploration use_best_candidate_to_explore: bool = True, # whether to use the best candidate as part of the exploration candidates memory_size: Optional[int] = None, # size of the heap memory to store the candidates; if None, no limit is set + score_function: str = 'mean', # function to compute the score for the candidates; 'mean' or 'ucb' + ucb_exploration_constant: float = 1.0, # exploration constant for UCB score function # Additional keyword arguments **kwargs ): @@ -193,11 +256,18 @@ def train(self, self.num_proposals = num_proposals self.validate_proposals = validate_proposals # whether to validate the proposed parameters self.use_best_candidate_to_explore = use_best_candidate_to_explore - self.default_score = default_score + self.score_function = score_function # function to compute the score for the candidates + if score_function == 'ucb': # this requires a bounded score range. By default, it is set to (0, 1) + if score_range is None: + score_range = (0, 1) + assert score_range[1]-score_range[0] < float('inf'), \ + "For UCB score function, score_range must be finite. Use 'mean' score function if you want to use unbounded scores." + + self.ucb_exploration_constant = 1. + self._exploration_candidates = None + self.memory = HeapMemory(size=memory_size) # Initialize the heap memory with a size limit - self.memory.push(self.default_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate - self._exploration_candidates = None super().train(guide, train_dataset, validate_dataset=validate_dataset, @@ -216,6 +286,7 @@ def train(self, save_path=save_path, **kwargs) + def update(self, samples=None, verbose=False, **kwargs): if samples is not None: @@ -225,6 +296,9 @@ def update(self, samples=None, verbose=False, **kwargs): validate_results = self.validate(candidates, samples, verbose=verbose, **kwargs) # this updates the priority queue # 3. Update the priority queue with the validation results self.update_memory(validate_results, verbose=verbose, **kwargs) # samples are provided here in case candidates do not capture full information + else: + if len(self.memory) == 0: + self.memory.push(self.max_score, ModuleCandidate(self.agent)) # Push the base agent as the first candidate (This gives the initialization of the priority queue) # 4. Explore and exploit the priority queue self._best_candidate, info_exploit = self.exploit(verbose=verbose, **kwargs) # get the best candidate (ModuleCandidate) from the priority queue self._exploration_candidates, info_explore = self.explore(verbose=verbose, **kwargs) # List of ModuleCandidates @@ -232,9 +306,9 @@ def update(self, samples=None, verbose=False, **kwargs): # TODO Log information about the update info_log = { - 'best_candidate_score': self._best_candidate.score(), - 'num_exploration_candidates': len(self._exploration_candidates), + 'n_iters': self.n_iters, # number of iterations } + info_log.update(info_exploit) # add the info from the exploit step info_log.update(info_explore) # add the info from the explore step return self._best_candidate.update_dict, [c.get_module() for c in self._exploration_candidates], info_log @@ -374,7 +448,6 @@ def validate(self, candidates, samples, verbose=False, **kwargs): # For example, it copies candidates. This would create a bug. return results - def update_memory(self, validate_results, verbose: bool = False, **kwargs): """ Update the priority queue with the validation results. Args: @@ -384,8 +457,8 @@ def update_memory(self, validate_results, verbose: bool = False, **kwargs): print("--- Updating memory with validation results...") if verbose else None for candidate, rollouts in validate_results.items(): candidate.add_rollouts(rollouts) # add the rollouts to the candidate - score = self.compute_score(candidate) # compute the score for the candidate - self.memory.push(score, candidate) + priority = self.compute_priority(candidate) # compute the priority for the candidate + self.memory.push(priority, candidate) #### def explore(self, verbose: bool = False, **kwargs): @@ -399,13 +472,25 @@ def explore(self, verbose: bool = False, **kwargs): print(f"--- Generating {min(len(self.memory), self.num_candidates)} exploration candidates...") if verbose else None # pop top self.num_candidates candidates from the priority queue top_candidates = [self._best_candidate] if self.use_best_candidate_to_explore else [] + priorities = [] # to store the priorities of the candidates while len(top_candidates) < self.num_candidates and self.memory: - score, candidate = self.memory.pop() # pop the top candidate from the priority queue + priority, candidate = self.memory.pop() # pop the top candidate from the priority queue + priority = - priority # remember that we stored negative scores in the priority queue + priorities.append(priority) # store the priority of the candidate if self.use_best_candidate_to_explore: if candidate == self._best_candidate: # skip if it is already in the top candidates continue top_candidates.append(candidate) # add the candidate to the top candidates - return top_candidates, {} + + mean_scores = [c.mean_score() for c in top_candidates] + mean_scores = [ s for s in mean_scores if s is not None] # filter out None scores + info_dict = { + 'num_exploration_candidates': len(top_candidates), + 'exploration_candidates_mean_priority': np.mean(priorities), # list of priorities of the exploration candidates + 'exploration_candidates_mean_score': np.mean(mean_scores), # list of mean scores of the exploration candidates + } + + return top_candidates, info_dict def exploit(self, verbose: bool = False, **kwargs): @@ -421,16 +506,14 @@ def exploit(self, verbose: bool = False, **kwargs): # This function can be overridden by subclasses to implement a different exploitation strategy if not self.memory: raise ValueError("The priority queue is empty. Cannot exploit.") - best = self.memory.best() # (score, candidate) - score, best_candidate = best - score = -score # remember that we stored negative scores in the priority queue + priority, best_candidate = self.memory.best() # (priority, candidate) + priority = - priority # remember that we stored negative scores in the priority queue return best_candidate, { - 'best_candidate_score': score, # remember that we stored negative scores in the priority queue + 'best_candidate_priority': priority, # remember that we stored negative scores in the priority queue + 'best_candidate_mean_score': best_candidate.mean_score(), # mean score of the candidate's rollouts } - - - def compute_score(self, candidate): + def compute_priority(self, candidate): # NOTE This function can be overridden by subclasses to compute a different score """ Compute the score for the candidate based on the rollouts during the validation phase. It can be overridden by subclasses to implement a different scoring strategy. @@ -444,7 +527,16 @@ def compute_score(self, candidate): raise TypeError("candidate must be an instance of ModuleCandidate.") # By default, we compute the mean score of the rollouts - scores = [r['score'] for r in candidate.rollouts] - default_score = self.default_score if self.default_score is not None else self.score_range[1] # default score for the candidates - - return np.mean(scores) if scores else self.default_score \ No newline at end of file + if self.score_function == 'mean': + # Compute the mean score of the candidate's rollouts + return candidate.mean_score() + elif self.score_function == 'ucb': + # Compute the Upper Confidence Bound (UCB) score + lcb_score, mean_score, ucb_score = candidate.compute_score_confidence( + min_score=self.min_score, + max_score=self.max_score, + scaling_constant=self.ucb_exploration_constant + ) + return ucb_score # return the UCB score + else: + raise ValueError(f"Unknown score function: {self.score_function}") diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/trainer/algorithms/priority_search/search_template.py index e37f8cff..d2b5e61c 100644 --- a/opto/trainer/algorithms/priority_search/search_template.py +++ b/opto/trainer/algorithms/priority_search/search_template.py @@ -92,14 +92,18 @@ def train(self, test_dataset = test_dataset or train_dataset # default to train_dataset if test_dataset is not provided test_guide = test_guide or guide self.num_eval_samples = num_eval_samples # number of samples to use to evaluate each input - self.score_range = score_range or (-np.inf, np.inf) + if score_range is None: + score_range = (-np.inf, np.inf) + assert len(score_range) == 2, "score_range must be a tuple (min_score, max_score)." + assert score_range[1] >= score_range[0], "score_range must be a tuple (min_score, max_score) with min_score <= max_score." + self._score_range = score_range # range of the score for the guide self.train_sampler = Sampler( DataLoader(train_dataset, batch_size=batch_size), guide, num_threads=self.num_threads, sub_batch_size=sub_batch_size, - score_range=self.score_range + score_range=self._score_range ) self._validate_dataset = validate_dataset # if None, the current batch will be used for validation self.validate_sampler = Sampler( @@ -107,7 +111,7 @@ def train(self, validate_guide or guide, num_threads=self.num_threads, sub_batch_size=None, # no sub-batch size for validation - score_range=self.score_range + score_range=self._score_range ) # Evaluate the agent before learning @@ -167,6 +171,16 @@ def train(self, self.n_iters += 1 return + @property + def max_score(self): + """ Maximum score that can be achieved by the agent. """ + return self._score_range[1] + + @property + def min_score(self): + """ Minimum score that can be achieved by the agent. """ + return self._score_range[0] + # Can be overridden by subclasses to implement specific sampling strategies def sample(self, agents, verbose=False, **kwargs): """ Sample a batch of data based on the proposed parameters. All proposals are evaluated on the same batch of inputs. @@ -183,6 +197,10 @@ def sample(self, agents, verbose=False, **kwargs): 'mean_score': np.mean(scores), 'n_epochs': self.train_sampler.n_epochs, } + # check if the scores are within the score range + if not (self.min_score <= log_info['mean_score'] <= self.max_score): + print(f"Warning: Mean score {log_info['mean_score']} is out of the range {self._score_range}.") + return samples, log_info def log(self, info_log, prefix=""): @@ -195,11 +213,14 @@ def log(self, info_log, prefix=""): print(e) def test(self, test_dataset, guide): - min_score = self.score_range[0] + min_score = self.min_score # Test the agent's performance test_score = self.evaluate(self.agent, guide, test_dataset['inputs'], test_dataset['infos'], min_score=min_score, num_threads=self.num_threads, description=f"Evaluating agent") # and log + # check if the test_score is within the score range + if not (self.min_score <= test_score <= self.max_score): + print(f"Warning: Test score {test_score} is out of the range {self._score_range}.") return {'test_score': test_score} def save(self, save_path): From 8c34f026e6684bcb1c8d88d4e6e27cd85c5a3c04 Mon Sep 17 00:00:00 2001 From: windweller Date: Mon, 4 Aug 2025 17:18:29 -0400 Subject: [PATCH 144/172] apply fix --- opto/trace/containers.py | 8 +++++++ tests/unit_tests/test_modules.py | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/opto/trace/containers.py b/opto/trace/containers.py index 402e39c8..f375ab47 100644 --- a/opto/trace/containers.py +++ b/opto/trace/containers.py @@ -41,15 +41,23 @@ def parameters_dict(self): both trainable and non-trainable parameters. The dict contains ParameterNodes or ParameterContainers. """ + from opto.trace.bundle import FunModule + parameters = {} for name, attr in inspect.getmembers(self): if name.startswith('__TRACE_RESERVED_'): # These are reserved for internal use. continue + if isinstance(attr, functools.partial): # this is a class method method = attr.func.__self__ if trainable_method(method): parameters[name] = method.parameter + elif isinstance(attr, FunModule): + # when a bundle method is not trainable + # it shows up as a FunModule attribute + if trainable_method(attr): + parameters[name] = attr.parameter elif trainable_method(attr): # method attribute parameters[name] = attr.parameter elif isinstance(attr, ParameterNode): diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index a1bbc17f..7e93f049 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -523,3 +523,40 @@ def forward(self, x): # Test that the copy can still function result = copied.forward(3) assert result._data == 34 # (3 * 8) + 10 + +def test_save_agent_xuanfei_case(): + + from typing import List, Dict, Any + from opto import trace + @trace.model + class SimpleAgent(): + """A simple test agent""" + + def __init__(self, tools_info: List[Dict[str, Any]]): + self.tools_info = trace.node(tools_info, trainable=True) + self.instructions = trace.node("Default instructions", trainable=True) + + @trace.bundle() + def solve(self, tools_info, instructions, task): + return f"Solved: {task} with {len(tools_info)} tools and instructions: {instructions}" + + def forward(self, task): + return self.solve(self.tools_info, self.instructions, task) + + def main(): + # Create agent + tools = [{"name": "test_tool", "description": "A test tool"}] + agent = SimpleAgent(tools) + + # Try to save agent using trace repo's built-in save method + print("\n--- Attempting to save agent ---") + agent.save("agent.pkl") + print("✅ Agent saved successfully using agent.save()") + + main() + import os + if os.path.exists("agent.pkl"): + os.remove("agent.pkl") + print("Temporary file 'agent.pkl' deleted.") + else: + print("File 'agent.pkl' does not exist.") \ No newline at end of file From 1a8608508bd4d7c7840bf0cc075c9d79eb835b8b Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 7 Aug 2025 23:20:55 +0000 Subject: [PATCH 145/172] Add save load methods to trainer algorithm, dataloader, optimizers, guide --- opto/optimizers/optimizer.py | 12 ++- opto/optimizers/optoprime.py | 58 +++++++++-- opto/optimizers/optoprime_v2.py | 46 ++++++++- opto/optimizers/textgrad.py | 26 ++++- opto/trainer/algorithms/UCBsearch.py | 86 ++++++++-------- opto/trainer/algorithms/algorithm.py | 75 ++++++++++++-- opto/trainer/guide.py | 27 +++-- opto/trainer/loader.py | 34 ++++++- tests/unit_tests/test_saving_loading.py | 126 +++++++++++++++++++++++- 9 files changed, 409 insertions(+), 81 deletions(-) diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index 77ee10db..04f8ea5e 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -54,7 +54,7 @@ def trace_graph(self): def step(self, bypassing=False, *args, **kwargs): update_dict = self.propose(*args, **kwargs) - self.project(update_dict) + self.project(update_dict) if not bypassing: self.update(update_dict) return update_dict # TODO add reasoning @@ -63,7 +63,7 @@ def project(self, update_dict: Dict[ParameterNode, Any]): """Project the update dictionary onto the feasible set.""" for p, d in update_dict.items(): if p.trainable: - for projection in p.projections: + for projection in p.projections: d = projection.project(d) update_dict[p] = d @@ -93,3 +93,11 @@ def default_propagator(self): def backward(self, node: Node, *args, **kwargs): """Propagate the feedback backward.""" return node.backward(*args, propagator=self.propagator, **kwargs) + + def save(self, path: str): + """Save the optimizer state to a file.""" + pass + + def load(self, path: str): + """Load the optimizer state from a file.""" + pass \ No newline at end of file diff --git a/opto/optimizers/optoprime.py b/opto/optimizers/optoprime.py index 6cbca909..6465151d 100644 --- a/opto/optimizers/optoprime.py +++ b/opto/optimizers/optoprime.py @@ -5,6 +5,7 @@ import json import re import copy +import pickle from opto.trace.nodes import ParameterNode, Node, MessageNode from opto.trace.propagators import TraceGraph, GraphPropagator from opto.trace.propagators.propagators import Propagator @@ -258,7 +259,7 @@ class OptoPrime(Optimizer): final_prompt_with_variables = dedent( """ What are your suggestions on variables {names}? - + Your response: """ ) @@ -333,13 +334,14 @@ def __init__( if prompt_symbols is not None: self.prompt_symbols.update(prompt_symbols) if json_keys is not None: - self.default_json_keys.update(json_keys) - if self.default_json_keys['answer'] is None: # answer field is not needed - del self.default_json_keys['answer'] - if 'answer' not in self.default_json_keys: + self.default_json_keys.update(json_keys) + # if self.default_json_keys['answer'] is None: + # del self.default_json_keys['answer'] + # NOTE del cause KeyError if the key is not in the dict due to changing class attribute + if 'answer' not in self.default_json_keys or self.default_json_keys['answer'] is None: # answer field is not needed # If 'answer' is not in the json keys, we use the no-answer format self.output_format_prompt = self.output_format_prompt_no_answer.format(**self.default_json_keys) - else: # If 'answer' is in the json keys, we use the original format of OptoPrime + else: # If 'answer' is in the json keys, we use the original format of OptoPrime self.output_format_prompt = self.output_format_prompt_original.format(**self.default_json_keys) self.use_json_object_format = use_json_object_format self.highlight_variables = highlight_variables @@ -450,8 +452,8 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): ) + user_prompt ) - - + + if self.highlight_variables: var_names = [] for k, v in summary.variables.items(): @@ -618,15 +620,51 @@ def call_llm( {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] - + response_format = {"type": "json_object"} if self.use_json_object_format else None try: # Try tp force it to be a json object response = self.llm(messages=messages, max_tokens=max_tokens, response_format=response_format) except Exception: response = self.llm(messages=messages, max_tokens=max_tokens) - + response = response.choices[0].message.content if verbose: print("LLM response:\n", response) return response + + + def save(self, path: str): + """Save the optimizer state to a file.""" + # save the above using pickle isntead + with open(path, "wb") as f: + pickle.dump( + { + "ignore_extraction_error": self.ignore_extraction_error, + "objective": self.objective, + "include_example": self.include_example, + "max_tokens": self.max_tokens, + "memory": self.memory, + "prompt_symbols": self.prompt_symbols, + "json_keys": self.default_json_keys, + 'output_format_prompt': self.output_format_prompt, + "use_json_object_format": self.use_json_object_format, + "highlight_variables": self.highlight_variables, + }, + f, + ) + + def load(self, path: str): + """Load the optimizer state from a file.""" + with open(path, "rb") as f: + state = pickle.load(f) + self.ignore_extraction_error = state["ignore_extraction_error"] + self.objective = state["objective"] + self.include_example = state["include_example"] + self.max_tokens = state["max_tokens"] + self.memory = state["memory"] + self.prompt_symbols = state["prompt_symbols"] + self.default_json_keys = state["json_keys"] + self.output_format_prompt = state['output_format_prompt'] + self.use_json_object_format = state["use_json_object_format"] + self.highlight_variables = state["highlight_variables"] \ No newline at end of file diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index db651bfb..cf0c81b0 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -12,7 +12,7 @@ from opto.utils.llm import AbstractModel, LLM from opto.optimizers.buffers import FIFOBuffer import copy - +import pickle import re from typing import Dict, Any @@ -343,7 +343,7 @@ class OptoPrimeV2(OptoPrime): For variables we express as this: {variable_expression_format} - + If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. """ ) @@ -354,7 +354,7 @@ class OptoPrimeV2(OptoPrime): output_format_prompt_template = dedent( """ Output_format: Your output should be in the following XML/HTML format: - + ``` {output_format} ``` @@ -407,7 +407,7 @@ class OptoPrimeV2(OptoPrime): final_prompt = dedent( """ What are your suggestions on variables {names}? - + Your response: """ ) @@ -710,3 +710,41 @@ def call_llm( if verbose: print("LLM response:\n", response) return response + + + def save(self, path: str): + """Save the optimizer state to a file.""" + with open(path, 'wb') as f: + pickle.dump({ + "truncate_expression": self.truncate_expression, + "use_json_object_format": self.use_json_object_format, + "ignore_extraction_error": self.ignore_extraction_error, + "objective": self.objective, + "initial_var_char_limit": self.initial_var_char_limit, + "optimizer_prompt_symbol_set": self.optimizer_prompt_symbol_set, + "include_example": self.include_example, + "max_tokens": self.max_tokens, + "memory": self.memory, + "default_prompt_symbols": self.default_prompt_symbols, + "prompt_symbols": self.prompt_symbols, + "representation_prompt": self.representation_prompt, + "output_format_prompt": self.output_format_prompt, + }, f) + + def load(self, path: str): + """Load the optimizer state from a file.""" + with open(path, 'rb') as f: + state = pickle.load(f) + self.truncate_expression = state["truncate_expression"] + self.use_json_object_format = state["use_json_object_format"] + self.ignore_extraction_error = state["ignore_extraction_error"] + self.objective = state["objective"] + self.initial_var_char_limit = state["initial_var_char_limit"] + self.optimizer_prompt_symbol_set = state["optimizer_prompt_symbol_set"] + self.include_example = state["include_example"] + self.max_tokens = state["max_tokens"] + self.memory = state["memory"] + self.default_prompt_symbols = state["default_prompt_symbols"] + self.prompt_symbols = state["prompt_symbols"] + self.representation_prompt = state["representation_prompt"] + self.output_format_prompt = state["output_format_prompt"] diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index bdfdeab4..9b7a1ef0 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -6,7 +6,7 @@ from opto.trace.propagators import TraceGraph, GraphPropagator, Propagator from opto.trace.utils import escape_json_nested_quotes, remove_non_ascii from opto.utils.llm import LLM, AbstractModel - +import pickle from copy import copy import re @@ -526,3 +526,27 @@ def call_llm( response = response.message.content return response + + + def save(self, path: str): + """ + Save the optimizer state to a file. + """ + with open(path, 'wb') as f: + pickle.dump({ + 'print_limit': self.print_limit, + 'max_tokens': self.max_tokens, + 'new_variable_tags': self.new_variable_tags, + 'optimizer_system_prompt': self.optimizer_system_prompt, + }, f) + + def load(self, path: str): + """ + Load the optimizer state from a file. + """ + with open(path, 'rb') as f: + state = pickle.load(f) + self.print_limit = state['print_limit'] + self.max_tokens = state['max_tokens'] + self.new_variable_tags = state['new_variable_tags'] + self.optimizer_system_prompt = state['optimizer_system_prompt'] \ No newline at end of file diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 9ff6f61b..036c19b3 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -34,13 +34,13 @@ def __init__(self, *args, **kwargs): super().__init__(agent, optimizer, num_threads=num_threads, logger=logger, *args, **kwargs) - - self.buffer = deque(maxlen=max_buffer_size) + + self.buffer = deque(maxlen=max_buffer_size) self.max_buffer_size = max_buffer_size # UCB exploration factor: Higher values encourage more exploration of less-tested candidates, - # lower values favor exploitation of well-performing candidates. + # lower values favor exploitation of well-performing candidates. self.ucb_exploration_factor = ucb_exploration_factor - + # To ensure optimizer_step can be called with bypassing=True if needed. # This depends on the specific optimizer's implementation. # For now, we assume the optimizer has a step method that can return parameters. @@ -55,7 +55,7 @@ def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> T if not dataset or not dataset.get('inputs') or not dataset.get('infos'): print_color("Warning: Attempted to sample from an empty or malformed dataset.", color='yellow') return [], [] - + dataset_size = len(dataset['inputs']) if dataset_size == 0: print_color("Warning: Dataset is empty, cannot sample minibatch.", color='yellow') @@ -67,8 +67,8 @@ def _sample_minibatch(self, dataset: Dict[str, List[Any]], batch_size: int) -> T infos = [dataset['infos'][i] for i in indices] return xs, infos - def _evaluate_candidate(self, - params_to_eval_dict: Dict[str, Any], + def _evaluate_candidate(self, + params_to_eval_dict: Dict[str, Any], dataset: Dict[str, List[Any]], # Changed from validate_dataset guide, # Changed from validate_guide evaluation_batch_size: int, # New parameter name @@ -80,13 +80,13 @@ def _evaluate_candidate(self, return -np.inf, 0 original_params = {p: copy.deepcopy(p.data) for p in self.optimizer.parameters} - self.optimizer.update(params_to_eval_dict) + self.optimizer.update(params_to_eval_dict) eval_xs, eval_infos = self._sample_minibatch(dataset, evaluation_batch_size) # Use evaluation_batch_size - + if not eval_xs: print_color("Evaluation minibatch is empty. Returning score -inf, count 0.", color='yellow') - self.optimizer.update(original_params) + self.optimizer.update(original_params) return -np.inf, 0 eval_scores = evaluate(self.agent, @@ -97,38 +97,38 @@ def _evaluate_candidate(self, num_threads=num_threads or self.num_threads, description=f"Evaluating candidate") - self.optimizer.update(original_params) + self.optimizer.update(original_params) avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf - eval_count = len(eval_xs) - + eval_count = len(eval_xs) + return float(avg_score), eval_count def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: """Calculates UCB score for a candidate in the buffer.""" if candidate_buffer_entry['eval_count'] == 0: return float('inf') # Explore unvisited states first - + mean_score = candidate_buffer_entry['score_sum'] / candidate_buffer_entry['eval_count'] - + # Add 1 to total_tracked_evaluations to prevent log(0) if it's the first evaluation overall # and to ensure log argument is > 0. # Add 1 to eval_count in denominator as well to ensure it's robust if eval_count is small. if total_tracked_evaluations == 0: # Should not happen if we init with one eval total_tracked_evaluations = 1 - + # UCB exploration term: ucb_exploration_factor scales the confidence interval # Higher factor = more exploration, lower factor = more exploitation exploration_term = self.ucb_exploration_factor * \ math.sqrt(math.log(total_tracked_evaluations) / candidate_buffer_entry['eval_count']) - + return mean_score + exploration_term def _update_buffer_ucb_scores(self): """Recalculates and updates UCB scores for all candidates in the buffer.""" if not self.buffer: return - + for candidate_entry in self.buffer: candidate_entry['ucb_score'] = self._calculate_ucb(candidate_entry, self._total_evaluations_tracker) @@ -138,9 +138,9 @@ def train(self, *, validation_dataset: Optional[Dict[str, List[Any]]] = None, # Validation set for evaluation, defaults to train_dataset num_search_iterations: int = 100, - train_batch_size: int = 2, + train_batch_size: int = 2, evaluation_batch_size: int = 20, # Renamed from validation_batch_size, used for all explicit evaluations - eval_frequency: int = 1, + eval_frequency: int = 1, log_frequency: Optional[int] = None, save_frequency: Optional[int] = None, save_path: str = "checkpoints/ucb_agent.pkl", @@ -155,7 +155,7 @@ def train(self, # Default validation_dataset to train_dataset if not provided if validation_dataset is None: validation_dataset = train_dataset - + num_threads = num_threads or self.num_threads log_frequency = log_frequency or eval_frequency self.min_score = min_score_for_agent_update # Used by parent's evaluate if called, or our own _evaluate_candidate @@ -176,7 +176,7 @@ def train(self, initial_score, initial_evals = self._evaluate_candidate( initial_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide ) - self._total_evaluations_tracker += initial_evals + self._total_evaluations_tracker += initial_evals total_samples += initial_evals # Log initial evaluation @@ -203,13 +203,13 @@ def train(self, # 1. Pick the candidate 'a' with the highest UCB from the buffer self._update_buffer_ucb_scores() # Ensure UCB scores are fresh action_candidate_a = self.select(self.buffer) - + # Log selected action UCB score self.logger.log('Selected action UCB', action_candidate_a['ucb_score'], iteration, color='magenta') self.logger.log('Selected action mean score', action_candidate_a['score_sum']/(action_candidate_a['eval_count'] or 1), iteration, color='cyan') - + print_color(f"Iter {iteration}/{num_search_iterations}: ", 'blue') - + # 2. Load parameters of 'a' into the agent for the optimizer update step self.optimizer.update(action_candidate_a['params']) @@ -218,7 +218,7 @@ def train(self, train_xs, train_infos = self._sample_minibatch(train_dataset, train_batch_size) if not train_xs: print_color(f"Iter {iteration}: Training minibatch empty, skipping optimizer step.", 'yellow') - continue + continue # Perform forward pass and get feedback for agent parameters 'a' outputs_for_a = [] @@ -236,7 +236,7 @@ def train(self, scores_from_train.append(score) targets_from_train.append(target) feedbacks_from_train.append(feedback) - + if not scores_from_train: # Should not happen if train_xs was not empty print_color(f"Iter {iteration}: No outputs from forward pass for candidate 'a'. Skipping.", 'yellow') continue @@ -249,7 +249,7 @@ def train(self, self.optimizer.backward(target_for_a, feedback_for_a) # Grads for 'a' are now in optimizer try: - a_prime_params_dict = self.optimizer.step(bypassing=True, verbose='output') + a_prime_params_dict = self.optimizer.step(bypassing=True, verbose='output') if not isinstance(a_prime_params_dict, dict) or not a_prime_params_dict: print_color(f"Iter {iteration}: Optimizer.step did not return a valid param dict for a_prime. Using current agent params as a_prime.", 'yellow') # Fallback: if step modified agent in-place and didn't return dict, current agent state is a_prime @@ -258,7 +258,7 @@ def train(self, except Exception as e: print_color(f"Iter {iteration}: Error during optimizer.step for a_prime: {e}. Skipping candidate generation.", 'red') continue - + # 4. Evaluate 'a_prime' on samples of validation set a_prime_score, a_prime_evals = self._evaluate_candidate( a_prime_params_dict, validation_dataset, guide, evaluation_batch_size, num_threads # Use validation_dataset and guide @@ -266,11 +266,11 @@ def train(self, self._total_evaluations_tracker += a_prime_evals total_samples += evaluation_batch_size + train_batch_size metrics['new_candidate_scores'].append(a_prime_score) - + # Log new candidate performance self.logger.log('New candidate score', a_prime_score, iteration, color='green') self.logger.log('Training batch score', score_for_a_on_train_batch, iteration, color='yellow') - + print_color(f"Iter {iteration}: New candidate a_prime generated. Validation Score: {a_prime_score:.4f}, Evals: {a_prime_evals}", 'cyan') # 5. Update the stats of 'a' (action_candidate_a) based on the training batch experience @@ -282,20 +282,20 @@ def train(self, # 6. Add 'a_prime' (with its validation stats) to the buffer if a_prime_score > -np.inf and a_prime_evals > 0: new_candidate_entry = { - 'params': a_prime_params_dict, + 'params': a_prime_params_dict, 'score_sum': a_prime_score * a_prime_evals, # Store sum 'eval_count': a_prime_evals, 'ucb_score': None, # avoid accidental reads before it's initializad 'iteration_created': iteration } - + # Eviction logic before adding if buffer is at max_len if len(self.buffer) == self.max_buffer_size: self._update_buffer_ucb_scores() # Ensure UCBs are current before eviction candidate_to_evict = min(self.buffer, key=lambda c: c['ucb_score']) self.buffer.remove(candidate_to_evict) print_color(f"Iter {iteration}: Buffer full. Evicted a candidate (UCB: {candidate_to_evict['ucb_score']:.4f})", 'magenta') - + self.buffer.append(new_candidate_entry) print_color(f"Iter {iteration}: Added new candidate to buffer.", 'magenta') else: @@ -322,7 +322,7 @@ def train(self, "total_evaluations_tracker": self._total_evaluations_tracker, "total_samples": total_samples # Add new metric } - + # Log all important metrics self.logger.log('Best candidate score', log_data['best_score'], iteration, color='green') self.logger.log('Buffer size', log_data['buffer_size'], iteration, color='blue') @@ -330,9 +330,9 @@ def train(self, self.logger.log('Buffer average evaluations', log_data['buffer_avg_evals'], iteration, color='orange') self.logger.log('Total evaluations tracker', log_data['total_evaluations_tracker'], iteration, color='magenta') self.logger.log('Total samples processed', log_data['total_samples'], iteration, color='yellow') - + print_color(f"Log @ Iter {iteration}: Best score in buffer: {log_data['best_score']:.4f}, Buffer size: {log_data['buffer_size']}, Total samples: {total_samples}", 'green') - + # Save agent (e.g., the one with highest mean score in buffer) if save_frequency is not None and iteration % save_frequency == 0: best_overall_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9) ) @@ -342,33 +342,33 @@ def train(self, # End of search loop print_color("UCB search finished.", 'blue') - + # Log final training summary final_iteration = num_search_iterations self.logger.log('UCB search completed', final_iteration, final_iteration, color='blue') self.logger.log('Final total samples', total_samples, final_iteration, color='magenta') - + if not self.buffer: print_color("Buffer is empty at the end of search. No best candidate found.", 'red') self.logger.log('Final status', 'Buffer empty - no best candidate', final_iteration, color='red') return metrics, -np.inf - + # Select the best candidate based on highest mean score (exploitation) final_best_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9)) final_best_score = final_best_candidate['score_sum'] / (final_best_candidate['eval_count'] or 1E-9) - + # Log final results self.logger.log('Final best score', final_best_score, final_iteration, color='green') self.logger.log('Final best candidate evaluations', final_best_candidate['eval_count'], final_iteration, color='cyan') self.logger.log('Final buffer size', len(self.buffer), final_iteration, color='blue') - + print_color(f"Final best candidate: Mean Score {final_best_score:.4f}, Evals {final_best_candidate['eval_count']}", 'green') # Load best parameters into the agent self.optimizer.update(final_best_candidate['params']) # Load params using optimizer return metrics, float(final_best_score) - + def select(self, buffer): '''Could be subclassed to implement different selection strategies''' return max(buffer, key=lambda c: c['ucb_score']) \ No newline at end of file diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 7995fc0b..b3506e23 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -1,8 +1,11 @@ from typing import Optional from opto.trace.modules import Module from opto.trainer.loggers import DefaultLogger +from opto.trainer.loader import DataLoader +from opto.trainer.guide import AutoGuide +from opto.optimizers.optimizer import Optimizer import os - +import pickle class AbstractAlgorithm: """ Abstract base class for all algorithms. """ @@ -38,10 +41,10 @@ def __init__(self, def _use_asyncio(self, threads=None): """Determine whether to use asyncio based on the number of threads. - + Args: threads: Number of threads to use. If None, uses self.num_threads. - + Returns: bool: True if parallel execution should be used, False otherwise. """ @@ -50,11 +53,11 @@ def _use_asyncio(self, threads=None): def save_agent(self, save_path, iteration=None): """Save the agent to the specified path. - + Args: save_path: Path to save the agent to. iteration: Current iteration number (for logging purposes). - + Returns: str: The path where the agent was saved. """ @@ -62,7 +65,7 @@ def save_agent(self, save_path, iteration=None): directory = os.path.dirname(save_path) if directory: os.makedirs(directory, exist_ok=True) - + # Add iteration number to filename if provided if iteration is not None: base, ext = os.path.splitext(save_path) @@ -71,14 +74,14 @@ def save_agent(self, save_path, iteration=None): save_path = f"{base}_iter{iteration}_final{ext}" else: save_path = f"{base}_iter{iteration}{ext}" - + # Save the agent self.agent.save(save_path) - + # Log if we have a logger and iteration is provided if hasattr(self, 'logger') and iteration is not None: self.logger.log('Saved agent', save_path, iteration, color='blue') - + return save_path def train(self, @@ -88,3 +91,57 @@ def train(self, **kwargs ): raise NotImplementedError + + + def save(self, path: str): + """ Save the guide to a file. """ + with open(path, 'wb') as f: + d = {} + for key, value in self.__dict__.items(): + if isinstance(value, Module): + _path = path+ f"_{key}.module" + value.save(_path) + d[key] = _path + elif isinstance(value, AutoGuide): + _path = path + f"_{key}.guide" + value.save(_path) + d[key] = _path + elif isinstance(value, DataLoader): + _path = path + f"_{key}.dataloader" + value.save(_path) + d[key] = _path + elif isinstance(value, Optimizer): + _path = path + f"_{key}.optimizer" + value.save(_path) + d[key] = _path + else: + d[key] = value + pickle.dump(d, f) + + def load(self, path: str): + """ Load the guide from a file. """ + with open(path, 'rb') as f: + data = pickle.load(f) + for key, value in data.items(): + if key not in self.__dict__: + warning_msg = f"Key '{key}' not found in the algorithm's attributes. Skipping loading for this key." + print(warning_msg) # or use logging.warning(warning_msg) + continue + + # key is in the algorithm's attributes + if isinstance(value, str): + if value.endswith('.module'): + attr = self.__dict__[key] + assert isinstance(attr, Module), f"Expected {key} to be a Module, got {type(attr)}" + elif value.endswith('.guide'): + attr = self.__dict__[key] + assert isinstance(attr, AutoGuide), f"Expected {key} to be an AutoGuide, got {type(attr)}" + elif value.endswith('.dataloader'): + attr = self.__dict__[key] + assert isinstance(attr, DataLoader), f"Expected {key} to be a DataLoader, got {type(attr)}" + elif value.endswith('.optimizer'): + attr = self.__dict__[key] + assert isinstance(attr, Optimizer), f"Expected {key} to be an Optimizer, got {type(attr)}" + attr.load(value) + else: + self.__dict__[key] = value \ No newline at end of file diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 8a727d86..df465cc8 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -1,5 +1,6 @@ from typing import List, Dict, Any, Union, Tuple, Optional, Callable import json +import pickle import re import copy from opto.utils.llm import LLM, AbstractModel @@ -37,15 +38,15 @@ def __call__(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float def forward(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]: return self.get_feedback(task, response, info, **kwargs) - + def get_feedback(self, query: str, response: str, reference: Optional[str] = None, **kwargs) -> Tuple[float, str]: raise NotImplementedError def metric(self, query: str, response: str, reference: Optional[str] = None, **kwargs) -> float: """ Exact match metric """ return self.get_feedback(query, response, reference)[0] - - def copy(self): + + def copy(self): """ Create a copy of the guide instance. Returns: @@ -55,6 +56,18 @@ def copy(self): # This can be overridden by subclasses to provide a more specific copy behavior. return copy.deepcopy(self) + def save(self, path: str): + """ Save the guide to a file. """ + with open(path, 'wb') as f: + pickle.dump(self.__dict__, f) + + def load(self, path: str): + """ Load the guide from a file. """ + with open(path, 'rb') as f: + data = pickle.load(f) + for key, value in data.items(): + setattr(self, key, value) + class VerbalJudgeGuide(AutoGuide): """ @@ -121,10 +134,10 @@ def get_feedback(self, query: str, response: str, reference: Optional[str] = Non # Check if metric function indicates perfect match user_prompt = self.prompt_template.format( - query=query, - response=response, - reference=reference, - correctness_template=self.DEFAULT_CORRECTNESS_TEMPLATE, + query=query, + response=response, + reference=reference, + correctness_template=self.DEFAULT_CORRECTNESS_TEMPLATE, incorrectness_template=self.DEFAULT_INCORRECTNESS_TEMPLATE) messages = [ diff --git a/opto/trainer/loader.py b/opto/trainer/loader.py index e61532b7..a2214297 100644 --- a/opto/trainer/loader.py +++ b/opto/trainer/loader.py @@ -1,7 +1,5 @@ import numpy as np - - - +import pickle class DataLoader: @@ -23,13 +21,16 @@ def __init__(self, dataset, batch_size=1, replacement=False, shuffle=True): self.replacement = replacement self.shuffle = shuffle self._indices = self._update_indices() + self._i = 0 def __iter__(self): indices = self._indices - for i in range(0, len(indices), self.batch_size): + for i in range(self._i, len(indices), self.batch_size): xs = [ self.dataset['inputs'][ind] for ind in indices[i:i + self.batch_size] ] infos = [self.dataset['infos'][ind] for ind in indices[i:i + self.batch_size] ] + self._i = i + self.batch_size yield xs, infos + self._i = 0 if self.shuffle: self._indices = self._update_indices() @@ -37,3 +38,28 @@ def __iter__(self): def _update_indices(self): N = len(self.dataset['inputs']) return np.random.choice(N, size=N, replace=self.replacement) + + def save(self, path): + """Save the dataset to a file.""" + with open(path, 'wb') as f: + pickle.dump( + {'_indices': self._indices, + '_i': self._i, + 'batch_size': self.batch_size, + 'replacement': self.replacement, + 'shuffle': self.shuffle, + 'dataset': self.dataset}, + f + ) + + def load(self, path): + """Load the dataset from a file.""" + import pickle + with open(path, 'rb') as f: + data = pickle.load(f) + self._indices = data['_indices'] + self._i = data['_i'] + self.batch_size = data['batch_size'] + self.replacement = data['replacement'] + self.shuffle = data['shuffle'] + self.dataset = data['dataset'] \ No newline at end of file diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index 1f634cd0..c0444512 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -1,6 +1,15 @@ from opto import trace +from opto.trainer.loader import DataLoader +from opto.trainer.algorithms import BasicSearchAlgorithm +from opto.optimizers import OptoPrimeV2 +from opto.trainer.guide import AutoGuide +from opto.utils.llm import DummyLLM + +import re, os +import numpy as np +import copy @trace.bundle(trainable=True) def fun(x): @@ -25,4 +34,119 @@ def test_saving_load(): a, b = fun(x) - print(a, b) \ No newline at end of file + print(a, b) + + +def test_trainer_saving_loading(): + + + class Guide(AutoGuide): + + def get_feedback(self, query, response, reference=None, **kwargs): + """ + Provide feedback based on the query and response. + + Args: + query: The query to analyze. + response: The response generated by the model. + reference: Optional reference answer for comparison. + **kwargs: Additional context or parameters. + + Returns: + A tuple containing a score and feedback string. + """ + score = response == reference + feedback = "Exact match!" if score == 1.0 else "Not an exact match." + return score, feedback + + @trace.model + class Agent: + + def __init__(self): + self.param = trace.node(1., trainable=True) + self.state = 0 + + def forward(self, x): + return self.param + 1 + + + xs = [1, 2, 3, 4, 5] + infos = [1, 2, 3, 4, 5] + batch_size = 3 + sub_batch_size = 2 + num_threads = 2 # 2 + dataset = {'inputs': xs, 'infos': infos} + loader = DataLoader(dataset, batch_size=batch_size) + num_proposals = 10 + num_candidates = 5 + memory_size = 3 + suggested_value = 5 + + + def _llm_callable(messages, **kwargs): + """ + A dummy LLM callable that simulates a response. + """ + problem = messages[1]['content'] + + # extract name from + name = re.findall(r"", problem) + if name: + name = name[0] + else: + name = "unknown" + + return f""" + Dummy reasoning based on the input messages. + + {name} + {suggested_value} + + """ + + # Create a dummy LLM and an agent + dummy_llm = DummyLLM(_llm_callable) + agent = Agent() + optimizer = OptoPrimeV2( + agent.parameters(), + llm=dummy_llm, + ) + optimizer.objective = 'fake objective' + algo = BasicSearchAlgorithm( + agent, + optimizer, + ) + + algo.train( + guide=Guide(), + train_dataset=dataset, + batch_size=batch_size, + num_threads=num_threads, + num_candidates=num_candidates, + num_proposals=num_proposals, + verbose=False, #'output', + ) + agent.param._data = 10 # to simulate a change in the agent's parameters + + algo.save('test_algo.pkl') + + + # Load the algorithm and check if it works + agent = Agent() + optimizer = OptoPrimeV2( + agent.parameters(), + llm=dummy_llm, + ) + algo2 = BasicSearchAlgorithm( + agent, + optimizer, + ) + algo2.load('test_algo.pkl') + + assert algo2.agent.param.data == 10, "Loaded agent's parameter does not match the saved one." + assert algo2.optimizer.objective == 'fake objective', "Loaded optimizer's objective does not match the saved one." + + os.remove('test_algo.pkl') + os.remove('test_algo.pkl_agent.module') + os.remove('test_algo.pkl_optimizer.optimizer') + os.remove('test_algo.pkl_validate_guide.guide') \ No newline at end of file From 6935652170d4bcf50b9a3ebfa9d15d78a6c69a4d Mon Sep 17 00:00:00 2001 From: Adith Swaminathan Date: Mon, 11 Aug 2025 17:56:43 -0700 Subject: [PATCH 146/172] Minor bugfixes to get examples and tests to work --- CONTRIBUTING.md | 2 +- README.md | 48 ++++++++++--------- SECURITY.md | 41 ---------------- SUPPORT.md | 25 ---------- examples/bbh/run_prompt_bigbench_dspy.py | 2 +- examples/bbh/run_prompt_bigbench_trace.py | 13 +++-- .../run_bigbench_trace_async.py | 2 +- examples/search_algo_example.py | 8 ++-- ...st_time_loss_for_code_OptoPrimeMulti.ipynb | 7 ++- opto/optimizers/textgrad.py | 1 + opto/trace/operators.py | 7 ++- opto/trainer/algorithms/UCBsearch.py | 2 +- pyproject.toml | 7 +-- setup.py | 2 +- tests/llm_optimizers_tests/test_guides.py | 19 ++++---- tests/llm_optimizers_tests/test_optimizer.py | 2 +- 16 files changed, 64 insertions(+), 124 deletions(-) delete mode 100644 SECURITY.md delete mode 100644 SUPPORT.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52182780..d7a907a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contribution Guideline -Trace is an actively growing project and under active maintenance and development! We maintain two major branches `main` and `experimental`. The `main` branch is the most stable, version-controlled branch and it is what the PyPI package is linked to. On the other hand, the `experimental` branch is the dev branch, which will change more dynamically in in preparation for the next version update. +Trace is an actively growing project and under active maintenance and development! We maintain two major branches `main` and `experimental`. The `main` branch is the most stable, version-controlled branch and it is what the PyPI package is linked to. On the other hand, the `experimental` branch is the dev branch, which will change more dynamically in preparation for the next version update. ### Development and Review Process diff --git a/README.md b/README.md index 724d6014..047f3c31 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- drawing + drawing

# End-to-end Generative Optimization for AI Agents @@ -15,10 +15,10 @@ losses, natural language text, compiler errors, etc.). Trace generalizes the bac propagating an AI system's execution trace. Trace is implemented as a PyTorch-like Python library. Users write Python code directly and can use Trace primitives to optimize certain parts, just like training neural networks! -[Paper](https://arxiv.org/abs/2406.16218) | [Project website](https://microsoft.github.io/Trace/) | [Documentation](https://microsoft.github.io/Trace/intro.html) | [Blogpost](https://www.microsoft.com/en-us/research/blog/tracing-the-path-to-self-adapting-ai-agents/) | [Discord channel](https://discord.gg/4VeAvwFcWy) | [Roadmap](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing) +[Paper](https://arxiv.org/abs/2406.16218) | [Project website](https://agentopt.github.io/Trace/) | [Documentation](https://agentopt.github.io/Trace/intro.html) | [Blogpost](https://www.microsoft.com/en-us/research/blog/tracing-the-path-to-self-adapting-ai-agents/) | [Discord channel](https://discord.gg/4VeAvwFcWy) | [Roadmap](https://docs.google.com/spreadsheets/d/1dMoECd2Soj6bATpkNDeaMxl0ymOYCtGq7ZiHr0JRdJU/edit?usp=sharing)

- drawing + drawing

## Setup @@ -104,6 +104,15 @@ test_output = strange_sort_list(test_input) print(test_output) ``` +Note that by default the generative optimizers in Trace (like OptoPrime) use LiteLLLM as the backend. +See [LLM API Setup](#llm-api-setup) below for more details. +At a minimum, the api_key must be set for calling LLMs, such as, + +```python +import os +os.environ['OPENAI_API_KEY'] = 'YOUR API KEY' +``` + Now, after declaring what is trainable and what isn't, and use `node` and `bundle` to define the computation graph, we can use the optimizer to optimize the computation graph. @@ -220,21 +229,15 @@ agent = train() Defining and training an agent through Trace will give you more flexibility and control over what the agent learns. -If you have a dataset and you want to use **multi-threading** to train and evaluate your workflow quickly: - -```python - -``` - ## Tutorials | **Level** | **Tutorial** | **Run in Colab** | **Description** | | --- |-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Beginner | [Getting Started](https://microsoft.github.io/Trace/quickstart/quick_start.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/Trace/blob/website/docs/quickstart/quick_start.ipynb) | Introduces basic primitives like `node` and `bundle`. Showcases a code optimization pipeline. | -| Beginner | [Adaptive AI Agent](https://microsoft.github.io/Trace/quickstart/quick_start_2.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/Trace/blob/website/docs/quickstart/quick_start_2.ipynb) | Introduce primitive `model` that allows anyone to build self-improving agents that react to environment feedback. Shows how an LLM agent learns to place a shot in a Battleship game. -| Intermediate | [Multi-Agent Collaboration](https://microsoft.github.io/Trace/quickstart/virtualhome.html) | N/A | Demonstrates how Trace can be used for multi-agent collaboration environment in Virtualhome. -| Intermediate | [NLP Prompt Optimization](https://microsoft.github.io/Trace/examples/nlp/bigbench_hard.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/Trace/blob/website/docs/examples/nlp/bigbench_hard.ipynb) | Shows how Trace can optimizes prompt and code together jointly for BigBench-Hard 23 tasks. -| Advanced | [Robotic Arm Control](https://microsoft.github.io/Trace/examples/robotics/metaworld.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/microsoft/Trace/blob/website/docs/examples/robotics/metaworld.ipynb) | Trace can optimize code to control a robotic arm after observing a full trajectory of interactions. | +| Beginner | [Getting Started](https://agentopt.github.io/Trace/quickstart/quick_start.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AgentOpt/Trace/blob/website/docs/quickstart/quick_start.ipynb) | Introduces basic primitives like `node` and `bundle`. Showcases a code optimization pipeline. | +| Beginner | [Adaptive AI Agent](https://agentopt.github.io/Trace/quickstart/quick_start_2.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AgentOpt/Trace/blob/website/docs/quickstart/quick_start_2.ipynb) | Introduce primitive `model` that allows anyone to build self-improving agents that react to environment feedback. Shows how an LLM agent learns to place a shot in a Battleship game. +| Intermediate | [Multi-Agent Collaboration](https://agentopt.github.io/Trace/quickstart/virtualhome.html) | N/A | Demonstrates how Trace can be used for multi-agent collaboration environment in Virtualhome. +| Intermediate | [NLP Prompt Optimization](https://agentopt.github.io/Trace/examples/nlp/bigbench_hard.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AgentOpt/Trace/blob/website/docs/examples/nlp/bigbench_hard.ipynb) | Shows how Trace can optimizes prompt and code together jointly for BigBench-Hard 23 tasks. +| Advanced | [Robotic Arm Control](https://agentopt.github.io/Trace/examples/robotics/metaworld.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AgentOpt/Trace/blob/website/docs/examples/robotics/metaworld.ipynb) | Trace can optimize code to control a robotic arm after observing a full trajectory of interactions. | ## Supported Optimizers @@ -274,7 +277,7 @@ The table evaluates the frameworks in the following aspects: We provide a comparison to validate our implementation of TextGrad in Trace:

- drawing + drawing

To produce this table, we ran the TextGrad pip-installed repo on 2024-10-30, and we also include the numbers reported in the TextGrad paper. @@ -389,8 +392,8 @@ Explains the role of feedback in LLM-based optimizers. An early work that influe ``` ## Contributors Wall - - + + ## Contributing @@ -404,8 +407,7 @@ a CLA and decorate the PR appropriately (e.g., status check, comment). Simply fo provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/). ## Roadmap @@ -423,7 +425,7 @@ please see the paper for details. which [fixes](https://platform.openai.com/docs/models/gpt-4o) the structured output issue of gpt-4o-2024-05-13. While gpt-4 works reliably most of the time, we've found gpt-4o-2024-05-13 often hallucinates even in very basic optimization problems and does not follow instructions. This might be due to the current implementation of optimizers -rely on outputing in json format. Issues of gpt-4o with json have been reported in the communities ( +rely on outputing in json format. Issues of gpt-4o with json have been reported in the community ( see [example](https://community.openai.com/t/gpt-4o-doesnt-consistently-respect-json-schema-on-tool-use/751125)). ## Disclaimers @@ -433,7 +435,7 @@ see [example](https://community.openai.com/t/gpt-4o-doesnt-consistently-respect- functionalities may be changed in the future. - System performance may vary by workflow, dataset, query, and response, and users are responsible for determining the accuracy of generated content. -- System outputs do not represent the opinions of Microsoft. +- System outputs do not represent the opinions of the developers of Trace. - All decisions leveraging outputs of the system should be made with human oversight and not be based solely on system outputs. - Use of the system must comply with all applicable laws, regulations, and policies, including those pertaining to @@ -446,10 +448,10 @@ see [example](https://community.openai.com/t/gpt-4o-doesnt-consistently-respect- This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft +Any use of Microsoft trademarks or logos in this project does not imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ## Privacy -See [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement). +This project has adopted the [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement). diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index b3c89efc..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). - - diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index 291d4d43..00000000 --- a/SUPPORT.md +++ /dev/null @@ -1,25 +0,0 @@ -# TODO: The maintainer of this repo has not yet edited this file - -**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - -- **No CSS support:** Fill out this template with information about how to file issues and get help. -- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. -- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. - -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* - -# Support - -## How to file issues and get help - -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. - -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE -FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER -CHANNEL. WHERE WILL YOU HELP PEOPLE?**. - -## Microsoft Support Policy - -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/examples/bbh/run_prompt_bigbench_dspy.py b/examples/bbh/run_prompt_bigbench_dspy.py index e1a1fa34..7b6a3e98 100644 --- a/examples/bbh/run_prompt_bigbench_dspy.py +++ b/examples/bbh/run_prompt_bigbench_dspy.py @@ -117,7 +117,7 @@ def evaluate_dp(dp, examples): stats = {} - llm = dspy.OpenAI(model="gpt-4-turbo-2024-04-09", max_tokens=512) + llm = dspy.LM(model="openai/gpt-4-turbo-2024-04-09", max_tokens=512) dspy.settings.configure(lm=llm) if args.cot: diff --git a/examples/bbh/run_prompt_bigbench_trace.py b/examples/bbh/run_prompt_bigbench_trace.py index c8f33467..d6b12047 100644 --- a/examples/bbh/run_prompt_bigbench_trace.py +++ b/examples/bbh/run_prompt_bigbench_trace.py @@ -4,6 +4,7 @@ from opto.optimizers import OptoPrime from datasets import load_dataset from opto.trace import model, bundle, ExecutionError +from opto.utils.llm import LLM import re from tqdm import tqdm @@ -24,10 +25,8 @@ def eval_metric(true, prediction): class LLMCallable: - def __init__(self, config_list=None, max_tokens=1024, verbose=False): - if config_list is None: - config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") - self.llm = autogen.OpenAIWrapper(config_list=config_list) + def __init__(self, llm=None, max_tokens=1024, verbose=False): + self.llm = llm or LLM() self.max_tokens = max_tokens self.verbose = verbose @@ -40,15 +39,15 @@ def call_llm(self, user_prompt): if self.verbose not in (False, "output"): print("Prompt\n", system_prompt + user_prompt) - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}] + messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, {"role": "user", "content": "Format your response as a JSON object."}] try: - response = self.llm.create( + response = self.llm( messages=messages, response_format={"type": "json_object"}, ) except Exception: - response = self.llm.create(messages=messages, max_tokens=self.max_tokens) + response = self.llm(messages=messages, max_tokens=self.max_tokens) response = response.choices[0].message.content if self.verbose: diff --git a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py index 7e12339f..3688907f 100644 --- a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py +++ b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py @@ -142,7 +142,7 @@ def forward(self, question): We read in a question and produces a response """ user_prompt = self.create_prompt(self.prompt_template, question) - response = trace_ops.call_llm(user_prompt) + response = trace_ops.call_llm(None, user_prompt) answer = self.extract_answer(self.prompt_template, question, response) return answer diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index ea3421c8..14fc61ea 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -215,7 +215,7 @@ def main(): help='Number of threads for parallel processing') parser.add_argument('--eval_frequency', type=int, default=2, help='How often to run evaluation') - parser.add_argument('--log_frequency', type=int, default=20, + parser.add_argument('--log_frequency', type=int, default=10, help='How often to log results') parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility') @@ -229,17 +229,17 @@ def main(): help='Maximum depth for beam search algorithms') parser.add_argument('--validation_dataset_size', type=int, default=20, help='Size of validation dataset for beam search') - parser.add_argument('--max_history_size', type=int, default=12, + parser.add_argument('--max_history_size', type=int, default=5, help='Maximum history size for history-based algorithms') parser.add_argument('--num_basicsearch_proposals', type=int, default=2, help='Number of proposals for basic search algorithm') # UCB algorithm-specific parameters - parser.add_argument('--max_buffer_size', type=int, default=10, + parser.add_argument('--max_buffer_size', type=int, default=5, help='Maximum buffer size for UCB algorithms') parser.add_argument('--ucb_exploration_factor', type=float, default=1.0, help='UCB exploration factor') - parser.add_argument('--num_search_iterations', type=int, default=100, + parser.add_argument('--num_search_iterations', type=int, default=4, help='Number of search iterations for UCB algorithms') parser.add_argument('--train_batch_size_ucb', type=int, default=2, help='Training batch size for UCB algorithms') diff --git a/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb b/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb index a5881d07..7cd54a63 100644 --- a/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb +++ b/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb @@ -405,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -453,6 +453,8 @@ } ], "source": [ + "import json\n", + "\n", "# Test all candidates and log execution times\n", "execution_results = []\n", "\n", @@ -462,7 +464,8 @@ " continue\n", "\n", " # Extract the function code from the dictionary\n", - " func_code = list(candidate.values())[0] # Assumes there's only one key-value pair in the dictionary\n", + " suggested_code = json.loads(candidate)[\"suggestion\"]\n", + " func_code = list(suggested_code.values())[0] # Assumes there's only one key-value pair in the dictionary\n", " if not func_code:\n", " print(f\"Candidate {i+1}: No code found\")\n", " continue\n", diff --git a/opto/optimizers/textgrad.py b/opto/optimizers/textgrad.py index bdfdeab4..7ca753fc 100644 --- a/opto/optimizers/textgrad.py +++ b/opto/optimizers/textgrad.py @@ -473,6 +473,7 @@ def _step(self, verbose=False): system_prompt=self.optimizer_system_prompt, verbose=verbose, ) + response = response.choices[0].message.content try: var_json = ( response.split(self.new_variable_tags[0])[1] diff --git a/opto/trace/operators.py b/opto/trace/operators.py index 45a2f715..2bab4980 100644 --- a/opto/trace/operators.py +++ b/opto/trace/operators.py @@ -588,10 +588,13 @@ def set_update(x: Any, y: Any): return x -@bundle() +@bundle(catch_execution_error=False) def call_llm(system_prompt, *user_prompts, **kwargs): """Query the language model of system_prompt with user_prompts.""" - messages = [{"role": "system", "content": system_prompt}] + if system_prompt is not None: + messages = [{"role": "system", "content": system_prompt}] + else: + messages = [{"role": "system", "content": "You are a helpful assistant.\n"}] for user_prompt in user_prompts: messages.append({"role": "user", "content": user_prompt}) from opto.utils.llm import LLM diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 9ff6f61b..dbc04cfe 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -99,7 +99,7 @@ def _evaluate_candidate(self, self.optimizer.update(original_params) - avg_score = np.mean(eval_scores) if eval_scores and all(s is not None for s in eval_scores) else -np.inf + avg_score = np.mean(eval_scores) if ((eval_scores is not None) and all(s is not None for s in eval_scores)) else -np.inf eval_count = len(eval_xs) return float(avg_score), eval_count diff --git a/pyproject.toml b/pyproject.toml index 8d652ed2..fa4852fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,12 +23,13 @@ classifiers = [ [project.optional-dependencies] autogen = ["autogen-agentchat==0.2.40"] +test = ["datasets==3.6.0"] [project.urls] -Homepage = "https://microsoft.github.io/Trace/" -Documentation = "https://microsoft.github.io/Trace/intro.html" -Repository = "https://github.com/microsoft/Trace.git" +Homepage = "https://agentopt.github.io/Trace/" +Documentation = "https://agentopt.github.io/Trace/intro.html" +Repository = "https://github.com/AgentOpt/Trace.git" [tool.setuptools] license-files = ["LICEN[CS]E*"] \ No newline at end of file diff --git a/setup.py b/setup.py index 4fa7eef5..e1e16725 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ install_requires = [ "graphviz>=0.20.1", "pytest", - "litellm", + "litellm==1.75.0", "black", "scikit-learn", "tensorboardX", diff --git a/tests/llm_optimizers_tests/test_guides.py b/tests/llm_optimizers_tests/test_guides.py index ec04b9f7..f3b6841c 100644 --- a/tests/llm_optimizers_tests/test_guides.py +++ b/tests/llm_optimizers_tests/test_guides.py @@ -7,7 +7,7 @@ def test_auto_guide_build(): assert reference_guide.model == "gpt-4" # Test building ReferenceGuide with custom prompt template - custom_prompt_guide = KeywordSuggest( + custom_prompt_guide = ReferenceSuggest( model="gpt-3.5-turbo", prompt_template="Custom prompt: {content}, Reference: {reference}" ) @@ -22,15 +22,12 @@ def test_auto_guide_build(): assert keyword_guide.keyword_response == keyword_response # Test building KeywordGuide with custom analyzers - # def custom_analyzer(content, reference_log): - # return "Custom analysis result" - # - # analyzer_guide = AutoGuide.build( - # keyword_response={"key": "value"}, - # custom_analyzers=[custom_analyzer] - # ) - # assert isinstance(analyzer_guide, KeywordGuide) - # assert len(analyzer_guide.custom_analyzers) == 1 - # assert analyzer_guide.custom_analyzers[0](None, None) == "Custom analysis result" + def custom_analyzer(content, reference_log): + return "Custom analysis result" + + analyzer_guide = KeywordSuggest(keyword_response={"key": "value"}, custom_analyzers=[custom_analyzer]) + assert isinstance(analyzer_guide, Suggest) + assert len(analyzer_guide.custom_analyzers) == 1 + assert analyzer_guide.custom_analyzers[0](None, None) == "Custom analysis result" # test_auto_guide_build() \ No newline at end of file diff --git a/tests/llm_optimizers_tests/test_optimizer.py b/tests/llm_optimizers_tests/test_optimizer.py index d78961c2..445d03e5 100644 --- a/tests/llm_optimizers_tests/test_optimizer.py +++ b/tests/llm_optimizers_tests/test_optimizer.py @@ -219,7 +219,7 @@ def test_optimizer_customization(optimizer_class): # Try to set custom parameters if the optimizer supports it try: if hasattr(optimizer_class, '__init__') and 'temperature' in inspect.signature(optimizer_class.__init__).parameters: - optimizer = optimizer_class([x], temperature=0.7) + optimizer = optimizer_class([x], temperature=0.0) else: optimizer = optimizer_class([x]) except Exception as e: From 62c9d3a0526912e27836d3628cc0c4913ff94545 Mon Sep 17 00:00:00 2001 From: Adith Swaminathan Date: Tue, 12 Aug 2025 19:35:14 -0700 Subject: [PATCH 147/172] Fixing notebooks in docs, adding tutorial for trainers --- docs/_config.yml | 8 +- docs/_toc.yml | 1 + docs/examples/basic/greeting.ipynb | 183 +- docs/examples/game/negotiation_arena.ipynb | 86 +- docs/examples/nlp/bigbench_hard.ipynb | 32 +- .../numerical/numerical_optimization.ipynb | 24 +- docs/examples/robotics/metaworld.ipynb | 39 +- docs/faq/faq.md | 5 +- docs/intro.md | 26 +- docs/quickstart/installation.md | 8 +- docs/quickstart/quick_start.ipynb | 17 +- docs/quickstart/quick_start_2.ipynb | 112 +- docs/quickstart/virtualhome.md | 9 +- docs/references.bib | 56 - docs/tutorials/error_handling_tutorial.ipynb | 64 +- docs/tutorials/minibatch.ipynb | 590 +++- docs/tutorials/optimization_tutorial.ipynb | 75 +- docs/tutorials/trainers.ipynb | 2860 +++++++++++++++++ examples/bbh/run_prompt_bigbench_trace.py | 1 - examples/virtualhome.py | 25 +- 20 files changed, 3767 insertions(+), 454 deletions(-) delete mode 100644 docs/references.bib create mode 100644 docs/tutorials/trainers.ipynb diff --git a/docs/_config.yml b/docs/_config.yml index 82390eee..53728795 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -23,7 +23,7 @@ latex: # Information about where the book exists on the web repository: - url: https://github.com/microsoft/Trace # Online location of your book + url: https://github.com/AgentOpt/Trace # Online location of your book path_to_book: docs # Optional path to your book, relative to the repository root branch: website # Which branch of the repository should be used when creating links (optional) @@ -36,9 +36,9 @@ html: use_issues_button: false use_repository_button: true extra_navbar: Go to Book Content - extra_footer: "Contact Us | Privacy & Cookies | Consumer Health Privacy | Terms Of Use | Trademarks" + extra_footer: "Contact Us | Terms Of Use | Trademarks" analytics: - plausible_analytics_domain: microsoft.github.io/trace + plausible_analytics_domain: agentopt.github.io/trace plausible_analytics_url: https://plausible.io/js/script.js sphinx: @@ -52,7 +52,7 @@ sphinx: - 'sphinx.ext.viewcode' config: add_module_names: false - plausible_domain: microsoft.github.io/trace + plausible_domain: agentopt.github.io/trace nb_merge_streams: true templates_path: ["_templates"] autosummary_generate: True diff --git a/docs/_toc.yml b/docs/_toc.yml index 47cf5984..990a135a 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -18,6 +18,7 @@ parts: - file: tutorials/error_handling_tutorial - file: tutorials/custom_optimizers - file: tutorials/minibatch + - file: tutorials/trainers - caption: Agent Examples numbered: false diff --git a/docs/examples/basic/greeting.ipynb b/docs/examples/basic/greeting.ipynb index 663773a8..0249a617 100644 --- a/docs/examples/basic/greeting.ipynb +++ b/docs/examples/basic/greeting.ipynb @@ -1,8 +1,9 @@ { "cells": [ { - "metadata": {}, "cell_type": "markdown", + "id": "a5a83b8093fae334", + "metadata": {}, "source": [ "# Greeting Agent\n", "\n", @@ -13,54 +14,102 @@ "## Setup and Installation\n", "\n", "Let's start by importing the necessary libraries." - ], - "id": "a5a83b8093fae334" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "%pip install trace-opt", - "id": "af6a991e6fa8e083" + "id": "af6a991e6fa8e083", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install trace-opt" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "500ce27b656605ea", + "metadata": {}, + "outputs": [], "source": [ - "%%capture\n", - "!pip install openai==1.55.3 httpx==0.27.2 --force-reinstall --quiet" - ], - "id": "500ce27b656605ea" + "%pip install openai httpx pywidgets" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "72b76d44a5423795", + "metadata": {}, + "outputs": [], "source": [ "from opto import trace\n", "from opto.trace import node, bundle, model, ExecutionError\n", "from opto.optimizers import OptoPrime" - ], - "id": "72b76d44a5423795" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "Add API keys for LLM calls. Run the code below:", - "id": "88243c6b69d0c2ad" + "id": "88243c6b69d0c2ad", + "metadata": {}, + "source": [ + "Add API keys for LLM calls. Run the code below:" + ] }, { + "cell_type": "code", + "execution_count": 1, + "id": "3242fb533b7cb3f4", "metadata": { "ExecuteTime": { "end_time": "2024-12-10T00:10:08.564966Z", "start_time": "2024-12-10T00:10:08.520705Z" } }, - "cell_type": "code", + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1bd6aa77089941b6bf1387d59df773d2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Text(value='OPENAI_API_KEY', description='Env Name:', placeholder='Enter env variable name (e.g., MY_API_KEY)'…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2c985d3f3ddd439bb6366c58833af31c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Password(description='API Key:', placeholder='Enter your API key')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "29026f7b286643a7bd31f4b2ac0533ff", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(description='Set API Key', style=ButtonStyle())" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -107,69 +156,24 @@ "\n", "# Attach the callback to the button\n", "submit_button.on_click(on_button_click)" - ], - "id": "3242fb533b7cb3f4", - "outputs": [ - { - "data": { - "text/plain": [ - "Text(value='OPENAI_API_KEY', description='Env Name:', placeholder='Enter env variable name (e.g., MY_API_KEY)'…" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "1bd6aa77089941b6bf1387d59df773d2" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Password(description='API Key:', placeholder='Enter your API key')" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "2c985d3f3ddd439bb6366c58833af31c" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Button(description='Set API Key', style=ButtonStyle())" - ], - "application/vnd.jupyter.widget-view+json": { - "version_major": 2, - "version_minor": 0, - "model_id": "29026f7b286643a7bd31f4b2ac0533ff" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "execution_count": 1 + ] }, { - "metadata": {}, "cell_type": "markdown", + "id": "753dc6c3e24a0899", + "metadata": {}, "source": [ "## Define an Agent\n", "\n", "In here, we use `@trace.bundle` to wrap functions so that they show up in TraceGraph. We use `trace.node` to wrap system prompts. `@trace.model` does not do much, except to provide us some convenience to grab all the trainable parameters. |" - ], - "id": "753dc6c3e24a0899" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "26064f7dfbd2ac2e", + "metadata": {}, + "outputs": [], "source": [ "@trace.model\n", "class Agent:\n", @@ -200,20 +204,22 @@ " \"\"\"Produce a greeting based on the language\"\"\"\n", " greeting = \"Hola\"\n", " return f\"{greeting}, {user_name}!\"" - ], - "id": "26064f7dfbd2ac2e" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "## Define Feedback and Training", - "id": "4d45873f3379d594" + "id": "4d45873f3379d594", + "metadata": {}, + "source": [ + "## Define Feedback and Training" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "43f743d5c27936c8", + "metadata": {}, + "outputs": [], "source": [ "def feedback_fn(generated_response, gold_label='en'):\n", " if gold_label == 'en' and 'Hello' in generated_response:\n", @@ -246,21 +252,22 @@ " break\n", "\n", " return agent" - ], - "id": "43f743d5c27936c8" + ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, - "source": "agent = train()", - "id": "ab2cb1b0c8a4f4b0" + "id": "ab2cb1b0c8a4f4b0", + "metadata": {}, + "outputs": [], + "source": [ + "agent = train()" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "trace", "language": "python", "name": "python3" }, @@ -274,7 +281,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", - "version": "2.7.6" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/examples/game/negotiation_arena.ipynb b/docs/examples/game/negotiation_arena.ipynb index 00f51823..4ea25a6e 100644 --- a/docs/examples/game/negotiation_arena.ipynb +++ b/docs/examples/game/negotiation_arena.ipynb @@ -12,34 +12,73 @@ "\n", "## Setup\n", "\n", - "First, we'll import the necessary packages and set up our environment." + "First, we'll import the necessary packages and set up our environment. Use the following cell to set the API key for LLM calls." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# Import necessary libraries\n", "import os\n", - "from openai import OpenAI\n", - "import json\n", - "\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "import opto.trace as trace\n", "from opto.optimizers import OptoPrime\n", - "from autogen import config_list_from_json\n", - "\n", - "config = config_list_from_json(\"OAI_CONFIG_LIST\")\n", - "key = None\n", - "for c in config:\n", - " if c['model'] == 'gpt-4-0125-preview':\n", - " key = c['api_key']\n", - " break\n", - "if key is None:\n", - " raise Exception(\"No key found for gpt-4-0125-preview in the provided config file\")\n", - "\n", - "client = OpenAI(api_key=key)\n" + "from opto.utils.llm import LLM\n", + "\n", + "client = LLM()\n" ] }, { @@ -179,10 +218,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "import json\n", + "\n", "@trace.bundle(trainable=False)\n", "def chat(player, message):\n", " global system_prompt\n", @@ -190,7 +231,7 @@ " global proposed_trade\n", " global proposed_end\n", " \n", - " current_message = [{'role': 'system', 'content': system_prompt}] + message\n", + " current_message = [{'role': 'system', 'content': system_prompt}, {\"role\": \"user\", \"content\": \"Format your response as a JSON object.\"}] + message\n", "\n", " if len(conversation) > 0:\n", " current_message.append({'role': 'user', 'content': 'This is the transcript of the conversation so far.'})\n", @@ -200,7 +241,6 @@ " current_message.append({'role': 'user', 'content': conversation_history})\n", "\n", " chat = client.chat.completions.create(\n", - " model='gpt-4-0125-preview',\n", " messages=current_message,\n", " temperature=0,\n", " max_tokens=200,\n", @@ -286,7 +326,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -329,7 +369,7 @@ "source": [ "# Initialize optimizer\n", "optimizer = OptoPrime(\n", - " [p1_prompt, p2_prompt], memory_size=0, config_list=config_list_from_json(\"OAI_CONFIG_LIST\")\n", + " [p1_prompt, p2_prompt], memory_size=0\n", " )\n", "\n", "# Run optimization loop\n", diff --git a/docs/examples/nlp/bigbench_hard.ipynb b/docs/examples/nlp/bigbench_hard.ipynb index e49d0cc3..00df852a 100644 --- a/docs/examples/nlp/bigbench_hard.ipynb +++ b/docs/examples/nlp/bigbench_hard.ipynb @@ -28,13 +28,12 @@ }, "outputs": [], "source": [ - "%pip install datasets\n", - "%pip install trace-opt" + "%pip install datasets trace-opt ipywidgets" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2024-07-19T21:19:03.229950Z", @@ -53,24 +52,20 @@ ], "source": [ "# Import necessary libraries\n", - "import autogen\n", "from opto.trace.nodes import node, GRAPH, ParameterNode\n", "from opto.optimizers import OptoPrime\n", "from datasets import load_dataset\n", "from textwrap import dedent\n", - "from opto.trace.bundle import bundle\n", - "from opto.trace.modules import model\n", - "from opto.trace.errors import ExecutionError\n", - "from opto.trace.nodes import ExceptionNode\n", - "from typing import List\n", + "from opto.trace import model, bundle, ExecutionError\n", + "from opto.utils.llm import LLM\n", "import re" ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -161,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2024-07-19T21:19:03.880915Z", @@ -171,10 +166,8 @@ "outputs": [], "source": [ "class LLMCallable:\n", - " def __init__(self, config_list=None, max_tokens=1024, verbose=False):\n", - " if config_list is None:\n", - " config_list = autogen.config_list_from_json(\"OAI_CONFIG_LIST\")\n", - " self.llm = autogen.OpenAIWrapper(config_list=config_list)\n", + " def __init__(self, llm=None, max_tokens=1024, verbose=False):\n", + " self.llm = llm or LLM()\n", " self.max_tokens = max_tokens\n", " self.verbose = verbose\n", "\n", @@ -182,7 +175,7 @@ " def call_llm(self, user_prompt):\n", " system_prompt = \"You are a helpful assistant.\\n\"\n", " messages = [{\"role\": \"system\", \"content\": system_prompt}, {\"role\": \"user\", \"content\": user_prompt}]\n", - " response = self.llm.create(messages=messages, max_tokens=self.max_tokens)\n", + " response = self.llm(messages=messages, max_tokens=self.max_tokens)\n", " response = response.choices[0].message.content\n", "\n", " if self.verbose:\n", @@ -311,7 +304,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2024-07-19T21:19:21.867979Z", @@ -941,8 +934,7 @@ "examples = [{\"question\": r[\"input\"], \"answer\": r[\"target\"]} for r in train_set]\n", "\n", "dp = Predict()\n", - "optimizer = OptoPrime(dp.parameters(),\n", - " config_list=autogen.config_list_from_json(\"OAI_CONFIG_LIST\"))\n", + "optimizer = OptoPrime(dp.parameters())\n", "\n", "print(\"Training on a few examples:\")\n", "train(dp, optimizer, examples[:5])\n", diff --git a/docs/examples/numerical/numerical_optimization.ipynb b/docs/examples/numerical/numerical_optimization.ipynb index 4a08530e..2160f85d 100644 --- a/docs/examples/numerical/numerical_optimization.ipynb +++ b/docs/examples/numerical/numerical_optimization.ipynb @@ -29,14 +29,12 @@ }, "outputs": [], "source": [ - "%pip install trace-opt\n", - "%pip install uxsim\n", - "%pip install numpy" + "%pip install trace-opt ipywidgets uxsim numpy" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -52,18 +50,16 @@ "import numpy as np\n", "import uxsim as ux\n", "import itertools\n", - "import opto\n", "import opto.trace as trace\n", "from opto.optimizers import OptoPrime\n", - "from opto.trace.bundle import ExceptionNode\n", - "from autogen import config_list_from_json" + "from opto.trace.bundle import ExceptionNode" ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -265,11 +261,11 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def run_approach(num_iter, trace_memory=0, trace_config=\"OAI_CONFIG_LIST\"):\n", + "def run_approach(num_iter, trace_memory=0):\n", " W = None\n", " return_val = np.zeros((num_iter, 3))\n", " \n", @@ -289,10 +285,10 @@ " return_dict = analyze_world(W)\n", " return return_dict\n", "\n", - " EW_x = trace.node(MIN_GREEN_TIME, trainable=True, constraint=f\"[{MIN_GREEN_TIME},{MAX_GREEN_TIME}]\")\n", - " NS_x = trace.node(MIN_GREEN_TIME, trainable=True, constraint=f\"[{MIN_GREEN_TIME},{MAX_GREEN_TIME}]\")\n", + " EW_x = trace.node(MIN_GREEN_TIME, trainable=True, description=f\"Value constrained to be within [{MIN_GREEN_TIME},{MAX_GREEN_TIME}]\")\n", + " NS_x = trace.node(MIN_GREEN_TIME, trainable=True, description=f\"Value constrained to be within [{MIN_GREEN_TIME},{MAX_GREEN_TIME}]\")\n", " optimizer = OptoPrime(\n", - " [EW_x, NS_x], memory_size=trace_memory, config_list=config_list_from_json(trace_config)\n", + " [EW_x, NS_x], memory_size=trace_memory\n", " )\n", "\n", " optimizer.objective = (\n", diff --git a/docs/examples/robotics/metaworld.ipynb b/docs/examples/robotics/metaworld.ipynb index b9342510..14c2829c 100644 --- a/docs/examples/robotics/metaworld.ipynb +++ b/docs/examples/robotics/metaworld.ipynb @@ -29,30 +29,29 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt ipywidgets" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "from autogen import config_list_from_json\n", "import llfbench\n", "import random\n", "import numpy as np\n", "import opto.trace as trace\n", "from opto.optimizers import OptoPrime\n", "from opto.trace.bundle import ExceptionNode\n", - "from opto.trace.errors import ExecutionError\n" + "from opto.trace.errors import ExecutionError" ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -234,7 +233,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -245,8 +244,7 @@ " n_optimization_steps=100,\n", " seed=0,\n", " relative=True,\n", - " verbose=False,\n", - " model=\"gpt-4-0125-preview\",\n", + " verbose=False\n", "):\n", "\n", " @trace.bundle(trainable=True)\n", @@ -261,9 +259,7 @@ " \"\"\"\n", " return [0, 0, 0, 0]\n", "\n", - " config_list = config_list_from_json(\"OAI_CONFIG_LIST\")\n", - " config_list = [config for config in config_list if config[\"model\"] == model]\n", - " optimizer = OptoPrime(controller.parameters(), config_list=config_list, memory_size=memory_size)\n", + " optimizer = OptoPrime(controller.parameters(), memory_size=memory_size)\n", "\n", " env = TracedEnv(env_name, seed=seed, relative=relative)\n", "\n", @@ -325,22 +321,22 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:32: UserWarning: \u001B[33mWARN: A Box observation space maximum and minimum values are equal. Actual equal coordinates: [(36,), (37,), (38,)]\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:32: UserWarning: \u001b[33mWARN: A Box observation space maximum and minimum values are equal. Actual equal coordinates: [(36,), (37,), (38,)]\u001b[0m\n", " logger.warn(\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:159: UserWarning: \u001B[33mWARN: The obs returned by the `reset()` method is not within the observation space.\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:159: UserWarning: \u001b[33mWARN: The obs returned by the `reset()` method is not within the observation space.\u001b[0m\n", " logger.warn(f\"{pre} is not within the observation space.\")\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:131: UserWarning: \u001B[33mWARN: The obs returned by the `reset()` method was expecting a numpy array, actual type: \u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:131: UserWarning: \u001b[33mWARN: The obs returned by the `reset()` method was expecting a numpy array, actual type: \u001b[0m\n", " logger.warn(\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/spaces/box.py:240: UserWarning: \u001B[33mWARN: Casting input x to numpy array.\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/spaces/box.py:240: UserWarning: \u001b[33mWARN: Casting input x to numpy array.\u001b[0m\n", " gym.logger.warn(\"Casting input x to numpy array.\")\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/core.py:311: UserWarning: \u001B[33mWARN: env.control_mode to get variables from other wrappers is deprecated and will be removed in v1.0, to get this variable you can do `env.unwrapped.control_mode` for environment variables or `env.get_wrapper_attr('control_mode')` that will search the reminding wrappers.\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/core.py:311: UserWarning: \u001b[33mWARN: env.control_mode to get variables from other wrappers is deprecated and will be removed in v1.0, to get this variable you can do `env.unwrapped.control_mode` for environment variables or `env.get_wrapper_attr('control_mode')` that will search the reminding wrappers.\u001b[0m\n", " logger.warn(\n" ] }, @@ -355,9 +351,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:159: UserWarning: \u001B[33mWARN: The obs returned by the `step()` method is not within the observation space.\u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:159: UserWarning: \u001b[33mWARN: The obs returned by the `step()` method is not within the observation space.\u001b[0m\n", " logger.warn(f\"{pre} is not within the observation space.\")\n", - "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:131: UserWarning: \u001B[33mWARN: The obs returned by the `step()` method was expecting a numpy array, actual type: \u001B[0m\n", + "/home/chinganc/miniconda3/envs/trace/lib/python3.8/site-packages/gymnasium/utils/passive_env_checker.py:131: UserWarning: \u001b[33mWARN: The obs returned by the `step()` method was expecting a numpy array, actual type: \u001b[0m\n", " logger.warn(\n" ] }, @@ -1469,8 +1465,7 @@ " memory_size=5,\n", " seed=0,\n", " relative=True,\n", - " verbose='output',\n", - " model=\"gpt-4-0125-preview\"\n", + " verbose='output'\n", ")\n" ] }, diff --git a/docs/faq/faq.md b/docs/faq/faq.md index 0c4e1926..7c6231e3 100644 --- a/docs/faq/faq.md +++ b/docs/faq/faq.md @@ -37,11 +37,8 @@ The table evaluates the frameworks in the following aspects: We provide a comparison to validate our implementation of TextGrad in Trace:

- drawing + drawing

To produce this table, we ran the TextGrad pip-installed repo on 2024-10-30, and we also include the numbers reported in the TextGrad paper. The LLM APIs are called around the same time to ensure a fair comparison. TextGrad paper's result was reported in 2024-06. - -## Difference to Libraries like AutoGen, AG2, OpenAI Swarm, Llama Stack - diff --git a/docs/intro.md b/docs/intro.md index 1fae750c..1f330d39 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -4,7 +4,7 @@ **It can record *traces* of operations on any Python objects and functions, and automatically construct an execution graph that is useful when LLMs are used as optimizers.** -Open In Colab +Open In Colab Our implementation is minimal and purely based on Python. It does not involve any API calls or library-specific dependencies, so it is composable with other libraries and tools. Trace features an API design inspired by PyTorch Autograd's gradient tape mechanism, which we adopted to reduce the learning curve of using Trace. @@ -29,20 +29,22 @@ After the user has declared the inputs and operations, Trace captures the execut Finally, the user can optimize the entire program, such as by updating the LLM instructions, using Trace. This step is the **optimize** phase. ```python +from opto import trace + @trace.model class Agent: def __init__(self, system_prompt): self.system_prompt = system_prompt self.instruct1 = trace.node("Decide the language", trainable=True) - self.instruct2 = trace.node("Extract name", trainable=True) + self.instruct2 = trace.node("Extract name if it's there", trainable=True) def __call__(self, user_query): # First LLM - response = call_llm(self.system_prompt, self.instruct1, user_query) + response = trace.operators.call_llm(self.system_prompt, self.instruct1, user_query) en_or_es = self.decide_lang(response) # Second LLM - user_name = call_llm(self.system_prompt, self.instruct2, user_query) + user_name = trace.operators.call_llm(self.system_prompt, self.instruct2, user_query) greeting = self.greet(en_or_es, user_name) return greeting @@ -63,16 +65,26 @@ Enabling traces of operations on Python objects allows us to capture the executi In the example below, we show how Trace can optimize an entire AI system end-to-end. ```python +from opto.optimizers import OptoPrime + +def feedback_fn(generated_response, gold_label='en'): + if gold_label == 'en' and 'Hello' in generated_response: + return "Correct" + elif gold_label == 'es' and 'Hola' in generated_response: + return "Correct" + else: + return "Incorrect" + agent = Agent("You are a sales assistant.") optimizer = OptoPrime(agent.parameters()) try: greeting = agent("Hola, soy Juan.") feedback = feedback_fn(greeting.data, 'es') - # feedback = "Correct" or "Incorrect" -except ExecutionError as e: + # feedback == "Correct" or "Incorrect" +except trace.ExecutionError as e: greeting = e.exception_node - feedback = greeting.data, + feedback = greeting.data optimizer.zero_feedback() optimizer.backward(greeting, feedback) diff --git a/docs/quickstart/installation.md b/docs/quickstart/installation.md index 1f4d1d1c..0c74a0ac 100644 --- a/docs/quickstart/installation.md +++ b/docs/quickstart/installation.md @@ -7,7 +7,7 @@ The ability to capture execution trace of Python program is defined in `opto.tra any external dependencies. However, if you want to use optimizer `opto.optimizers`, -then we require `autogen` package to make LLM API calls. +then we require `LiteLLM` package to make LLM API calls. To install Trace, run: @@ -19,12 +19,8 @@ pip install trace-opt To contribute to the development, you can clone the repository and install the package in editable mode: -```{tip} -The installation script will git clone a version of AutoGen. -You may require Git Large File Storage if git is unable to clone the repository otherwise. - ```bash -git clone https://github.com/microsoft/Trace.git +git clone https://github.com/AgentOpt/Trace.git cd Trace pip install -e . ``` \ No newline at end of file diff --git a/docs/quickstart/quick_start.ipynb b/docs/quickstart/quick_start.ipynb index 5a93a08c..ce8b17a4 100644 --- a/docs/quickstart/quick_start.ipynb +++ b/docs/quickstart/quick_start.ipynb @@ -28,14 +28,16 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt\n", + "%pip install ipywidgets" ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "150ebe0c019eb767", + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -82,8 +84,7 @@ "\n", "# Attach the callback to the button\n", "submit_button.on_click(on_button_click)" - ], - "id": "150ebe0c019eb767" + ] }, { "cell_type": "markdown", @@ -358,7 +359,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "81d783e1-462f-4938-9f2c-389b4b546a74", "metadata": {}, "outputs": [ @@ -372,7 +373,6 @@ } ], "source": [ - "import autogen\n", "from opto.optimizers import OptoPrime\n", "from opto import trace\n", "\n", @@ -901,7 +901,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "db1ea71c-9a4e-4e14-9584-b19bff6ae1ae", "metadata": {}, "outputs": [ @@ -921,7 +921,6 @@ } ], "source": [ - "import autogen\n", "from opto.optimizers import OptoPrime\n", "\n", "GRAPH.clear()\n", diff --git a/docs/quickstart/quick_start_2.ipynb b/docs/quickstart/quick_start_2.ipynb index b668a2c4..0c9c1e1b 100644 --- a/docs/quickstart/quick_start_2.ipynb +++ b/docs/quickstart/quick_start_2.ipynb @@ -27,7 +27,8 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt\n", + "%pip install ipywidgets" ] }, { @@ -45,10 +46,11 @@ ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "id": "dea338357fc76304", + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import ipywidgets as widgets\n", @@ -95,8 +97,7 @@ "\n", "# Attach the callback to the button\n", "submit_button.on_click(on_button_click)" - ], - "id": "dea338357fc76304" + ] }, { "cell_type": "markdown", @@ -122,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "0edf4c3f", "metadata": {}, "outputs": [], @@ -132,7 +133,7 @@ "import importlib.util\n", "\n", "# Define the raw URL for downloading\n", - "raw_url = \"https://raw.githubusercontent.com/microsoft/Trace/main/examples/battleship.py\"\n", + "raw_url = \"https://raw.githubusercontent.com/agentopt/Trace/main/examples/battleship.py\"\n", "\n", "# Define the local file path\n", "local_file = \"battleship.py\"\n", @@ -609,7 +610,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "e4536733-89c0-4245-802b-d5812dd38d0c", "metadata": { "editable": true, @@ -2190,7 +2191,6 @@ } ], "source": [ - "import autogen\n", "from opto.trace.utils import render_opt_step\n", "from battleship import BattleshipBoard\n", "\n", @@ -2295,7 +2295,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 19, "id": "16daeec5-27ef-44c7-9395-cc6a7264e230", "metadata": { "editable": true, @@ -2372,14 +2372,14 @@ " \n", " \n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", "\n", "
ABCDEFGH
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
" ], @@ -2456,7 +2456,7 @@ " \n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -2538,8 +2538,8 @@ " \n", "
ABCDEFGH
1
2
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2621,8 +2621,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2704,8 +2704,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2787,8 +2787,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2870,8 +2870,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -2953,8 +2953,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3036,8 +3036,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3119,8 +3119,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3202,8 +3202,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3285,8 +3285,8 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3368,9 +3368,9 @@ " \n", "
ABCDEFGH
1
2
1
2
3
4
5
\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3451,9 +3451,9 @@ " \n", "
ABCDEFGH
1
2
3
1
2
3
4
5
6
\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3534,9 +3534,9 @@ " \n", "
ABCDEFGH
1
2
3
1
2
3
4
5
6
\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3617,9 +3617,9 @@ " \n", "
ABCDEFGH
1
2
3
1
2
3
4
5
6
\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", "\n", @@ -3656,14 +3656,6 @@ " if terminal:\n", " break" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "34cd5783-c79c-4f7e-b6a5-a4a138103927", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -3682,7 +3674,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/quickstart/virtualhome.md b/docs/quickstart/virtualhome.md index 799fbf15..3f48a184 100644 --- a/docs/quickstart/virtualhome.md +++ b/docs/quickstart/virtualhome.md @@ -130,19 +130,22 @@ optimizer1 = OptoPrime([agent1.plan]) optimizer2 = OptoPrime([agent2.plan]) agents = [agent1, agent2] +optimizers = [optimizer1, optimizer2] ``` We then run the simulation for a fixed number of steps. In each step, we observe the environment, and each agent produces an action based on its observation. ```python -from examples.virtualhome import VirtualHomeEnv +from examples.virtualhome import VirtualHomeEnv, env_fn horizon = 50 +task_id = 8 -env = VirtualHomeEnv() +env = VirtualHomeEnv(max_number_steps=horizon, run_id=0, env_fn=env_fn(env_id=0, env_task_set=task_id), + agent_fn=agents, num_agents=len(agents)) # we specify a task in this environment -agent_obs, agent_obs_descs, agent_goal_specs, agent_goal_descs, agent_infos = env.reset(task_id=8) +agent_obs, agent_obs_descs, agent_goal_specs, agent_goal_descs, agent_infos = env.reset(task_id=task_id) for h in range(horizon): plans, errors = {}, {} diff --git a/docs/references.bib b/docs/references.bib deleted file mode 100644 index 783ec6aa..00000000 --- a/docs/references.bib +++ /dev/null @@ -1,56 +0,0 @@ ---- ---- - -@inproceedings{holdgraf_evidence_2014, - address = {Brisbane, Australia, Australia}, - title = {Evidence for {Predictive} {Coding} in {Human} {Auditory} {Cortex}}, - booktitle = {International {Conference} on {Cognitive} {Neuroscience}}, - publisher = {Frontiers in Neuroscience}, - author = {Holdgraf, Christopher Ramsay and de Heer, Wendy and Pasley, Brian N. and Knight, Robert T.}, - year = {2014} -} - -@article{holdgraf_rapid_2016, - title = {Rapid tuning shifts in human auditory cortex enhance speech intelligibility}, - volume = {7}, - issn = {2041-1723}, - url = {http://www.nature.com/doifinder/10.1038/ncomms13654}, - doi = {10.1038/ncomms13654}, - number = {May}, - journal = {Nature Communications}, - author = {Holdgraf, Christopher Ramsay and de Heer, Wendy and Pasley, Brian N. and Rieger, Jochem W. and Crone, Nathan and Lin, Jack J. and Knight, Robert T. and Theunissen, Frédéric E.}, - year = {2016}, - pages = {13654}, - file = {Holdgraf et al. - 2016 - Rapid tuning shifts in human auditory cortex enhance speech intelligibility.pdf:C\:\\Users\\chold\\Zotero\\storage\\MDQP3JWE\\Holdgraf et al. - 2016 - Rapid tuning shifts in human auditory cortex enhance speech intelligibility.pdf:application/pdf} -} - -@inproceedings{holdgraf_portable_2017, - title = {Portable learning environments for hands-on computational instruction using container-and cloud-based technology to teach data science}, - volume = {Part F1287}, - isbn = {978-1-4503-5272-7}, - doi = {10.1145/3093338.3093370}, - abstract = {© 2017 ACM. There is an increasing interest in learning outside of the traditional classroom setting. This is especially true for topics covering computational tools and data science, as both are challenging to incorporate in the standard curriculum. These atypical learning environments offer new opportunities for teaching, particularly when it comes to combining conceptual knowledge with hands-on experience/expertise with methods and skills. Advances in cloud computing and containerized environments provide an attractive opportunity to improve the effciency and ease with which students can learn. This manuscript details recent advances towards using commonly-Available cloud computing services and advanced cyberinfrastructure support for improving the learning experience in bootcamp-style events. We cover the benets (and challenges) of using a server hosted remotely instead of relying on student laptops, discuss the technology that was used in order to make this possible, and give suggestions for how others could implement and improve upon this model for pedagogy and reproducibility.}, - booktitle = {{ACM} {International} {Conference} {Proceeding} {Series}}, - author = {Holdgraf, Christopher Ramsay and Culich, A. and Rokem, A. and Deniz, F. and Alegro, M. and Ushizima, D.}, - year = {2017}, - keywords = {Teaching, Bootcamps, Cloud computing, Data science, Docker, Pedagogy} -} - -@article{holdgraf_encoding_2017, - title = {Encoding and decoding models in cognitive electrophysiology}, - volume = {11}, - issn = {16625137}, - doi = {10.3389/fnsys.2017.00061}, - abstract = {© 2017 Holdgraf, Rieger, Micheli, Martin, Knight and Theunissen. Cognitive neuroscience has seen rapid growth in the size and complexity of data recorded from the human brain as well as in the computational tools available to analyze this data. This data explosion has resulted in an increased use of multivariate, model-based methods for asking neuroscience questions, allowing scientists to investigate multiple hypotheses with a single dataset, to use complex, time-varying stimuli, and to study the human brain under more naturalistic conditions. These tools come in the form of “Encoding” models, in which stimulus features are used to model brain activity, and “Decoding” models, in which neural features are used to generated a stimulus output. Here we review the current state of encoding and decoding models in cognitive electrophysiology and provide a practical guide toward conducting experiments and analyses in this emerging field. Our examples focus on using linear models in the study of human language and audition. We show how to calculate auditory receptive fields from natural sounds as well as how to decode neural recordings to predict speech. The paper aims to be a useful tutorial to these approaches, and a practical introduction to using machine learning and applied statistics to build models of neural activity. The data analytic approaches we discuss may also be applied to other sensory modalities, motor systems, and cognitive systems, and we cover some examples in these areas. In addition, a collection of Jupyter notebooks is publicly available as a complement to the material covered in this paper, providing code examples and tutorials for predictive modeling in python. The aimis to provide a practical understanding of predictivemodeling of human brain data and to propose best-practices in conducting these analyses.}, - journal = {Frontiers in Systems Neuroscience}, - author = {Holdgraf, Christopher Ramsay and Rieger, J.W. and Micheli, C. and Martin, S. and Knight, R.T. and Theunissen, F.E.}, - year = {2017}, - keywords = {Decoding models, Encoding models, Electrocorticography (ECoG), Electrophysiology/evoked potentials, Machine learning applied to neuroscience, Natural stimuli, Predictive modeling, Tutorials} -} - -@book{ruby, - title = {The Ruby Programming Language}, - author = {Flanagan, David and Matsumoto, Yukihiro}, - year = {2008}, - publisher = {O'Reilly Media} -} diff --git a/docs/tutorials/error_handling_tutorial.ipynb b/docs/tutorials/error_handling_tutorial.ipynb index 1eeaafd9..8ec81fe1 100644 --- a/docs/tutorials/error_handling_tutorial.ipynb +++ b/docs/tutorials/error_handling_tutorial.ipynb @@ -20,7 +20,67 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt ipywidgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below provides a way to specify your API_KEY for calling LLMs using LiteLLM as part of this tutorial notebook. Alternatively, provide the keys by setting environment variables or loading LiteLLM config files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" ] }, { @@ -257,7 +317,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/tutorials/minibatch.ipynb b/docs/tutorials/minibatch.ipynb index 80fd12b9..dd1ad029 100644 --- a/docs/tutorials/minibatch.ipynb +++ b/docs/tutorials/minibatch.ipynb @@ -11,58 +11,73 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "collapsed": false }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Looking in indexes: https://pypi.netflix.net/simple\n", - "Requirement already satisfied: trace-opt in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (0.1.1)\n", - "Requirement already satisfied: autogen-agentchat~=0.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from trace-opt) (0.2.37)\n", - "Requirement already satisfied: graphviz>=0.20.1 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from trace-opt) (0.20.3)\n", - "Requirement already satisfied: scikit-learn in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from trace-opt) (1.5.1)\n", - "Requirement already satisfied: xgboost in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from trace-opt) (2.1.1)\n", - "Requirement already satisfied: diskcache in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (5.6.3)\n", - "Requirement already satisfied: docker in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (7.1.0)\n", - "Requirement already satisfied: flaml in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (2.3.1)\n", - "Requirement already satisfied: numpy<2,>=1.17.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (1.26.4)\n", - "Requirement already satisfied: openai>=1.3 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (1.52.2)\n", - "Requirement already satisfied: packaging in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (24.1)\n", - "Requirement already satisfied: pydantic!=2.6.0,<3,>=1.10 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (2.9.2)\n", - "Requirement already satisfied: python-dotenv in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (1.0.1)\n", - "Requirement already satisfied: termcolor in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (2.5.0)\n", - "Requirement already satisfied: tiktoken in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from autogen-agentchat~=0.2->trace-opt) (0.8.0)\n", - "Requirement already satisfied: scipy>=1.6.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from scikit-learn->trace-opt) (1.13.1)\n", - "Requirement already satisfied: joblib>=1.2.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from scikit-learn->trace-opt) (1.4.2)\n", - "Requirement already satisfied: threadpoolctl>=3.1.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from scikit-learn->trace-opt) (3.5.0)\n", - "Requirement already satisfied: anyio<5,>=3.5.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (4.6.2.post1)\n", - "Requirement already satisfied: distro<2,>=1.7.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (1.9.0)\n", - "Requirement already satisfied: httpx<1,>=0.23.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (0.27.2)\n", - "Requirement already satisfied: jiter<1,>=0.4.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (0.6.1)\n", - "Requirement already satisfied: sniffio in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (1.3.1)\n", - "Requirement already satisfied: tqdm>4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (4.66.6)\n", - "Requirement already satisfied: typing-extensions<5,>=4.11 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from openai>=1.3->autogen-agentchat~=0.2->trace-opt) (4.12.2)\n", - "Requirement already satisfied: annotated-types>=0.6.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from pydantic!=2.6.0,<3,>=1.10->autogen-agentchat~=0.2->trace-opt) (0.7.0)\n", - "Requirement already satisfied: pydantic-core==2.23.4 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from pydantic!=2.6.0,<3,>=1.10->autogen-agentchat~=0.2->trace-opt) (2.23.4)\n", - "Requirement already satisfied: requests>=2.26.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from docker->autogen-agentchat~=0.2->trace-opt) (2.32.3)\n", - "Requirement already satisfied: urllib3>=1.26.0 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from docker->autogen-agentchat~=0.2->trace-opt) (2.2.3)\n", - "Requirement already satisfied: regex>=2022.1.18 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from tiktoken->autogen-agentchat~=0.2->trace-opt) (2024.9.11)\n", - "Requirement already satisfied: idna>=2.8 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from anyio<5,>=3.5.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (3.7)\n", - "Requirement already satisfied: exceptiongroup>=1.0.2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from anyio<5,>=3.5.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (1.2.2)\n", - "Requirement already satisfied: certifi in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (2024.8.30)\n", - "Requirement already satisfied: httpcore==1.* in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (1.0.6)\n", - "Requirement already satisfied: h11<0.15,>=0.13 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai>=1.3->autogen-agentchat~=0.2->trace-opt) (0.14.0)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /home/aswaminathan/miniconda3/envs/trace/lib/python3.10/site-packages (from requests>=2.26.0->docker->autogen-agentchat~=0.2->trace-opt) (3.3.2)\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], + "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt ipywidgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a preamble, the code below provides a way to specify your API_KEY for calling LLMs using LiteLLM as part of this tutorial notebook. Alternatively, provide the keys by setting environment variables or loading LiteLLM config files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" ] }, { @@ -74,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ @@ -100,19 +115,12 @@ "def loss(y_hat, y):\n", " \"\"\" A least squares loss function. \"\"\"\n", " return (y_hat - y) ** 2\n", - "\n", - "\n", - "def compute_loss(inputs, outputs):\n", - " l = 0\n", - " for x,y in zip(inputs, outputs):\n", - " y_hat = fun(x)\n", - " l += loss(y_hat, y)\n", - " return l\n" + "\n" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 34, "metadata": {}, "outputs": [ { @@ -120,24 +128,25 @@ "output_type": "stream", "text": [ "Iteration 0 Loss: 85\n", - "Iteration 1 Loss: 10\n", + "Iteration 1 Loss: 85\n", "Iteration 2 Loss: 10\n", - "Iteration 3 Loss: 7.5\n", - "Iteration 4 Loss: 122.8125\n", - "Iteration 5 Loss: 80.3125\n", - "Iteration 6 Loss: 12.8125\n", - "Iteration 7 Loss: 10.0\n", - "Iteration 8 Loss: 7.5\n", - "Iteration 9 Loss: 8.150000000000002\n", - "Iteration 10 Loss: 6.449999999999999\n", - "Iteration 11 Loss: 8.150000000000002\n", - "Iteration 12 Loss: 9.037500000000001\n", - "Iteration 13 Loss: 9.427\n" + "Iteration 3 Loss: 15\n", + "Iteration 4 Loss: 10\n", + "Iteration 5 Loss: 40\n", + "Iteration 6 Loss: 0\n", + "Iteration 7 Loss: 0\n", + "Iteration 8 Loss: 0\n", + "Iteration 9 Loss: 0\n", + "Iteration 10 Loss: 0\n", + "Iteration 11 Loss: 0\n", + "Iteration 12 Loss: 0\n", + "Iteration 13 Loss: 0\n", + "Iteration 14 Loss: 0\n" ] }, { "data": { - "image/png": "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+5yUlJTglltuwe7du/GXv/wFoVBI/tqbl5cHk8mkVNnJp+wwmLb927/9m1hWViaaTCbxoosuSusxawCDvr344otKl6YqHD2P+K//+i9x6tSpotlsFqurq8W1a9cqXZKiurq6xIceekgcP368mJGRIU6YMEFcvny56PP5lC4tqd57771Bv47cc889oihGxs9/8pOfiA6HQzSbzeJVV10lfvrpp8oWnWBn+5w0NDQM+bX3vffeU7r0pBJEURSTGa6IiIiIkok9O0RERKRpDDtERESkaQw7REREpGkMO0RERKRpDDtERESkaQw7REREpGkMO0RERKRpDDtERESkaQw7REQAysvLsWrVKqXLIKIEYNghoqS79957cfPNNwMAampqsGzZsqR97Jdeegk5OTkDHt+xYwe+9a1vJa0OIkoeXgRKRJrg9/vP62LDwsLCOFZDRGrClR0iUsy9996LzZs341e/+hUEQYAgCDh69CgA4PPPP8d1112HrKwsFBcX46677kJbW5v8Z2tqavDAAw/g4YcfRkFBARYsWAAAWLlyJaZNmwar1YrS0lIsWbIEHo8HAPD+++/jn/7pn+ByueSPV1tbC2DgNtaxY8fwta99DVlZWcjOzsatt96KlpYW+fna2lrMnDkTL7/8MsrLy2G323HbbbfB7XYn9pNGRCPGsENEivnVr36Fyy67DP/8z/+M5uZmNDc3o7S0FM3NzZg3bx5mzpyJnTt34m9/+xtaWlpw66239vvz69atg8FgwIcffojf/OY3AACdTodf//rX+Oyzz7Bu3Tq8++67ePTRRwEAc+fOxapVq5CdnS1/vEceeWRAXaIo4uabb8bp06exefNmbNy4EYcPH8Y3vvGNfq87fPgw3nzzTfzlL3/BX/7yF2zevBk///nPE/TZIqLR4jYWESnGbrfDZDLBYrHA4XDIjz/33HO46KKLUFdXJz/2u9/9DqWlpTh48CAuuOACAEBlZSVWrFjR733G9v9UVFTgpz/9Kf7v//2/ePbZZ2EymWC32yEIQr+Pd6ZNmzbhk08+QUNDA0pLSwEAL7/8MqZMmYIdO3bg4osvBgCEw2G89NJLsNlsAIC77roL77zzDp566qnz+8QQUVxxZYeIVGfXrl147733kJWVJb9VV1cDiKymSObMmTPgz7733ntYsGABxo4dC5vNhrvvvhvt7e3wer3D/vj79+9HaWmpHHQA4MILL0ROTg72798vP1ZeXi4HHQAYM2YMWltbR/R3JaLE48oOEalOOBzGjTfeiKeffnrAc2PGjJF/bbVa+z3X2NiI6667Dvfffz9++tOfIi8vD1u3bsU3v/lNBAKBYX98URQhCMI5Hzcajf2eFwQB4XB42B+HiJKDYYeIFGUymRAKhfo9dtFFF+FPf/oTysvLYTAM/8vUzp07EQwG8cwzz0Cniyxc//GPfzznxzvThRdeiGPHjqGpqUle3fn888/hcrkwefLkYddDROrAbSwiUlR5eTn+93//F0ePHkVbWxvC4TCWLl2K06dP4/bbb8f27dtx5MgRbNiwAffdd99Zg8rEiRMRDAaxevVqHDlyBC+//DKef/75AR/P4/HgnXfeQVtbG7q7uwe8n/nz52P69Om44447sHv3bmzfvh1333035s2bN+jWGRGpG8MOESnqkUcegV6vx4UXXojCwkIcO3YMJSUl+PDDDxEKhfDVr34VU6dOxUMPPQS73S6v2Axm5syZWLlyJZ5++mlMnToVf/jDH1BfX9/vNXPnzsX999+Pb3zjGygsLBzQ4AxEtqPefPNN5Obm4qqrrsL8+fMxYcIEvPbaa3H/+xNR4gmiKIpKF0FERESUKFzZISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJNY9ghIiIiTWPYISIiIk1j2CEiIiJN+/8Bb4UU3ujdoe4AAAAASUVORK5CYII=", + "image/png": "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FUD4sSIDIKKqq6t18cUXa9asWUe8LycnJ/T/U1JSWrxvx44d+t73vqdf/vKXmjFjhrp27aply5bphhtuUENDg5KTkzu1zvj4+Ba/drlcCgaDnfoaAE4cQQZA2CQkJCgQCLR47LTTTtP//u//Kj8/Xx5P238ErVmzRsFgUI888ojc7ubF5FdeeeW4r/dtgwYNUnFxsYqLi0OrMps2bVJFRYUGDx7c5noARAe2lgCETX5+vlauXKkdO3aovLxcwWBQkydP1v79+3XVVVdp9erV+vLLL/X222/r+uuvP2YI6d+/vxobG/X4449r27ZteuGFF0JNwIe/XnV1tYqKilReXt7qllNhYaEKCgo0adIkrV27VqtWrdI111yjc845RyNHjuz07wGA8CLIAAibX//614qLi9PgwYPVvXt37dq1S7m5ufrggw8UCAR0wQUXqKCgQFOmTFFmZmZopaU1w4cP15w5czRr1iwNHTpUL774ombOnNniY8aOHatf/OIXuuKKK9S9e/cjmoWl5i2iv/3tb+rSpYvOPvtsFRYWql+/fnr55Zc7/esHEH4uwzAMq4sAAADoCFZkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbRFkAACAbf1/vMD8m5kd3FAAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -154,6 +163,13 @@ " \"\"\" A linear predictor function \"\"\"\n", " return 0\n", "\n", + "def compute_loss(inputs, outputs):\n", + " l = 0\n", + " for x,y in zip(inputs, outputs):\n", + " y_hat = fun(x)\n", + " l += loss(y_hat, y)\n", + " return l\n", + "\n", "optimizer = OptoPrime(fun.parameters())\n", "\n", "ls = []\n", @@ -189,7 +205,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -197,24 +213,25 @@ "output_type": "stream", "text": [ "Iteration 0 Loss: 85\n", - "Iteration 1 Loss: 15\n", + "Iteration 1 Loss: 10\n", "Iteration 2 Loss: 10\n", - "Iteration 4 Loss: 10\n", - "Iteration 5 Loss: 6\n", - "Iteration 6 Loss: 6\n", - "Iteration 7 Loss: 5\n", - "Iteration 8 Loss: 5\n", - "Iteration 9 Loss: 1\n", - "Iteration 10 Loss: 0\n", - "Iteration 11 Loss: 0\n", - "Iteration 12 Loss: 0\n", - "Iteration 13 Loss: 9\n", - "Iteration 14 Loss: 120\n" + "Iteration 3 Loss: 120\n", + "Iteration 4 Loss: 120\n", + "Iteration 5 Loss: 120\n", + "Iteration 6 Loss: 60\n", + "Iteration 7 Loss: 30\n", + "Iteration 8 Loss: 30\n", + "Iteration 9 Loss: 15\n", + "Iteration 10 Loss: 10\n", + "Iteration 11 Loss: 10\n", + "Iteration 12 Loss: 15\n", + "Iteration 13 Loss: 55\n", + "Iteration 14 Loss: 15\n" ] }, { "data": { - "image/png": "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6ld2JGnRokW64YYbNGnSJJ133nlatWqVysrKdOutt5pdGgAAMJklws4111yjQ4cO6b777pPH41FhYaH+/ve/67TTTjO7NAAAYDJLzNk5WczZAQAg+pzo7++o79kBAAD4MoQdAABgaYQdAABgaYQdAABgaYQdAABgaYQdAABgaYQdAABgaYQdAABgaYQdAABgaZa4LuJkBYdI19bWmlwJAAA4UcHf2191GQRhR1JdXZ0kKS8vz+RKAABAb9XV1cnpdB73de7GkhQIBFRRUaH09HTZbLawfd3a2lrl5eWpvLycO7eOwvvSE+/JsfG+9MR70hPvybHFwvtiGIbq6uqUm5sru/34nTms7Eiy2+069dRT++3rZ2RkWPZftJPB+9IT78mx8b70xHvSE+/JsVn9ffmyFZ0gGpQBAIClEXYAAIClEXb6kcPh0C9+8Qs5HA6zS4kovC898Z4cG+9LT7wnPfGeHBvvSxcalAEAgKWxsgMAACyNsAMAACyNsAMAACyNsAMAACyNsNOPHnnkEY0YMUJJSUmaOHGi3nrrLbNLMk1xcbHOPfdcpaena+jQoZozZ4527dpldlkRpbi4WDabTQsXLjS7FNMdOHBA3/72t5Wdna2UlBSNHz9eW7ZsMbss07S3t+unP/2pRowYoeTkZI0cOVL33XefAoGA2aUNqDfffFOzZ89Wbm6ubDabXnzxxW6vG4ahpUuXKjc3V8nJyZo+fbp27NhhTrED5Mvek7a2Nt19990aO3asUlNTlZubqxtvvFEVFRXmFWwSwk4/ee6557Rw4UItWbJEH3zwgS688EJddtllKisrM7s0U2zYsEELFizQu+++q5KSErW3t2vWrFlqaGgwu7SIsHnzZq1atUpnn3222aWYrqamRueff74SEhL0yiuv6JNPPtFDDz2kzMxMs0szzYMPPqjHHntMK1eu1M6dO7Vs2TL96le/0sMPP2x2aQOqoaFB48aN08qVK4/5+rJly7R8+XKtXLlSmzdvltvt1syZM0P3H1rRl70njY2N2rp1q372s59p69atev7557V7925deeWVJlRqMgP9YvLkycatt97a7bmCggLjnnvuMamiyFJVVWVIMjZs2GB2Kaarq6sz8vPzjZKSEmPatGnGHXfcYXZJprr77ruNCy64wOwyIsoVV1xh3HLLLd2emzt3rvHtb3/bpIrMJ8l44YUXQn8OBAKG2+02HnjggdBzzc3NhtPpNB577DETKhx4X3xPjmXTpk2GJGPfvn0DU1SEYGWnH7S2tmrLli2aNWtWt+dnzZqljRs3mlRVZPH5fJKkrKwskysx34IFC3TFFVdoxowZZpcSEV566SVNmjRJ3/rWtzR06FBNmDBBjz/+uNllmeqCCy7Q66+/rt27d0uSPvzwQ7399tu6/PLLTa4scpSWlsrr9Xb7uetwODRt2jR+7h7F5/PJZrPF3EopF4H2g+rqavn9frlcrm7Pu1wueb1ek6qKHIZhaNGiRbrgggtUWFhodjmmevbZZ7V161Zt3rzZ7FIixp49e/Too49q0aJF+slPfqJNmzbpBz/4gRwOh2688UazyzPF3XffLZ/Pp4KCAsXFxcnv9+v+++/XddddZ3ZpESP4s/VYP3f37dtnRkkRp7m5Wffcc4/mzZtn6YtBj4Ww049sNlu3PxuG0eO5WHTbbbfpo48+0ttvv212KaYqLy/XHXfcoXXr1ikpKcnsciJGIBDQpEmTVFRUJEmaMGGCduzYoUcffTRmw85zzz2np59+WmvWrNGYMWO0bds2LVy4ULm5ubrpppvMLi+i8HP32Nra2nTttdcqEAjokUceMbucAUfY6QeDBw9WXFxcj1WcqqqqHv/VEWtuv/12vfTSS3rzzTd16qmnml2OqbZs2aKqqipNnDgx9Jzf79ebb76plStXqqWlRXFxcSZWaI6cnByNHj2623NnnXWW/vrXv5pUkfl+/OMf65577tG1114rSRo7dqz27dun4uJiwk4nt9stqWOFJycnJ/Q8P3c7gs7VV1+t0tJSvfHGGzG3qiNxGqtfJCYmauLEiSopKen2fElJiaZOnWpSVeYyDEO33Xabnn/+eb3xxhsaMWKE2SWZ7uKLL9bHH3+sbdu2hR6TJk3S9ddfr23btsVk0JGk888/v8dYgt27d+u0004zqSLzNTY2ym7v/uM6Li4u5o6ef5kRI0bI7XZ3+7nb2tqqDRs2xOzPXakr6Pzzn//Ua6+9puzsbLNLMgUrO/1k0aJFuuGGGzRp0iSdd955WrVqlcrKynTrrbeaXZopFixYoDVr1uhvf/ub0tPTQ6teTqdTycnJJldnjvT09B49S6mpqcrOzo7pXqYf/vCHmjp1qoqKinT11Vdr06ZNWrVqlVatWmV2aaaZPXu27r//fg0bNkxjxozRBx98oOXLl+uWW24xu7QBVV9fr88++yz059LSUm3btk1ZWVkaNmyYFi5cqKKiIuXn5ys/P19FRUVKSUnRvHnzTKy6f33Ze5Kbm6urrrpKW7du1csvvyy/3x/62ZuVlaXExESzyh545h4Gs7b//M//NE477TQjMTHROOecc2L6mLWkYz6eeOIJs0uLKBw97/A///M/RmFhoeFwOIyCggJj1apVZpdkqtraWuOOO+4whg0bZiQlJRkjR440lixZYrS0tJhd2oBav379MX+O3HTTTYZhdBw//8UvfmG43W7D4XAYX//6142PP/7Y3KL72Ze9J6Wlpcf92bt+/XqzSx9QNsMwjIEMVwAAAAOJnh0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AAGBphB0AkDR8+HCtWLHC7DIA9APCDoABd/PNN2vOnDmSpOnTp2vhwoUD9r2ffPJJZWZm9nh+8+bN+t73vjdgdQAYOFwECsASWltbT+piwyFDhoSxGgCRhJUdAKa5+eabtWHDBv3mN7+RzWaTzWbT3r17JUmffPKJLr/8cqWlpcnlcumGG25QdXV16HOnT5+u2267TYsWLdLgwYM1c+ZMSdLy5cs1duxYpaamKi8vT/Pnz1d9fb0k6R//+If+7d/+TT6fL/T9li5dKqnnNlZZWZm++c1vKi0tTRkZGbr66qtVWVkZen3p0qUaP368nnrqKQ0fPlxOp1PXXnut6urq+vdNA9BrhB0ApvnNb36j8847T9/97nfl8Xjk8XiUl5cnj8ejadOmafz48Xr//fe1du1aVVZW6uqrr+72+atXr1Z8fLzeeecd/e53v5Mk2e12/fa3v9X27du1evVqvfHGG7rrrrskSVOnTtWKFSuUkZER+n533nlnj7oMw9CcOXN0+PBhbdiwQSUlJfr88891zTXXdPu4zz//XC+++KJefvllvfzyy9qwYYMeeOCBfnq3APQV21gATON0OpWYmKiUlBS53e7Q848++qjOOeccFRUVhZ774x//qLy8PO3evVtnnnmmJOmMM87QsmXLun3No/t/RowYoV/+8pf693//dz3yyCNKTEyU0+mUzWbr9v2+6LXXXtNHH32k0tJS5eXlSZKeeuopjRkzRps3b9a5554rSQoEAnryySeVnp4uSbrhhhv0+uuv6/777z+5NwZAWLGyAyDibNmyRevXr1daWlroUVBQIKljNSVo0qRJPT53/fr1mjlzpk455RSlp6frxhtv1KFDh9TQ0HDC33/nzp3Ky8sLBR1JGj16tDIzM7Vz587Qc8OHDw8FHUnKyclRVVVVr/6uAPofKzsAIk4gENDs2bP14IMP9ngtJycn9M+pqandXtu3b58uv/xy3XrrrfrlL3+prKwsvf322/rOd76jtra2E/7+hmHIZrN95fMJCQndXrfZbAoEAif8fQAMDMIOAFMlJibK7/d3e+6cc87RX//6Vw0fPlzx8Sf+Y+r9999Xe3u7HnroIdntHQvXf/nLX77y+33R6NGjVVZWpvLy8tDqzieffCKfz6ezzjrrhOsBEBnYxgJgquHDh+u9997T3r17VV1drUAgoAULFujw4cO67rrrtGnTJu3Zs0fr1q3TLbfc8qVB5fTTT1d7e7sefvhh7dmzR0899ZQee+yxHt+vvr5er7/+uqqrq9XY2Njj68yYMUNnn322rr/+em3dulWbNm3SjTfeqGnTph1z6wxAZCPsADDVnXfeqbi4OI0ePVpDhgxRWVmZcnNz9c4778jv9+uSSy5RYWGh7rjjDjmdztCKzbGMHz9ey5cv14MPPqjCwkL9+c9/VnFxcbePmTp1qm699VZdc801GjJkSI8GZ6ljO+rFF1/UoEGD9PWvf10zZszQyJEj9dxzz4X97w+g/9kMwzDMLgIAAKC/sLIDAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAsjbADAAAs7f8DZuPK6GmbydUAAAAASUVORK5CYII=", + "image/png": "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SEhAWloapk2bhv3794suS5innnoKkiRhzpw5okuJqOPHj+OnP/0pUlJSEBsbi6FDh2L79u2iy4oIv9+PefPmIT8/H7GxsejTpw8ef/zxs97fo2eff/45rr32WmRlZUGSJKxYsaLT62VZxmOPPYbMzEzExsZi8uTJOHjwoJhiw+BMX7/P58PDDz+MoUOHwuFwICsrC7fddhtKS0vFFRwGZ/s70NHPf/5zSJKEJUuWRKQ2hp0wefvttzF37lz89re/xc6dOzF8+HBMmTIFbrdbdGlht27dOsyaNQubN2/G6tWr4fP5cPnll6OhoUF0aRG3bds2/OlPf8KwYcNElxJRJ0+exIQJE2C1WvGvf/0L33zzDZ5++mkkJSWJLi0iFi1ahGXLluGFF17Avn37sGjRIixevBjPP/+86NLCpqGhAcOHD8eLL754ytcvXrwYS5cuxfLly7FlyxY4HA5MmTIFXq83wpWGx5m+/sbGRuzcuRPz5s3Dzp078e6772L//v247rrrBFQaPmf7O6B47733sHnzZmRlZUWoMgAyhcWYMWPkWbNmqX/2+/1yVlaWvHDhQoFVieF2u2UA8rp160SXElF1dXVyv3795NWrV8uXXXaZfP/994suKWIefvhh+eKLLxZdhjBXX321fNddd3V67Prrr5dnzJghqKLIAiC/99576p8DgYCckZEh//d//7f6WE1NjWy32+W//e1vAioMr+9+/aeydetWGYBcVFQUmaIi7HTfg2PHjsk9e/aU9+7dK+fm5srPPvtsROphZycMWlpasGPHDkyePFl9zGQyYfLkydi0aZPAysSora0FACQnJwuuJLJmzZqFq6++utPfg2jxwQcfYNSoUfjJT36CtLQ0jBw5En/+859FlxUx48ePx5o1a3DgwAEAwO7du7F+/XpceeWVgisTo7CwEGVlZZ3+LbhcLowdOzYqnxOB4POiJElITEwUXUrEBAIB3HrrrXjwwQcxePDgiH5uXgQaBpWVlfD7/UhPT+/0eHp6Or799ltBVYkRCAQwZ84cTJgwAUOGDBFdTsS89dZb2LlzJ7Zt2ya6FCGOHDmCZcuWYe7cufjVr36Fbdu24b777oPNZsPtt98uurywe+SRR+DxeFBQUACz2Qy/348FCxZgxowZoksToqysDABO+ZyovC6aeL1ePPzww7jlllui6mLQRYsWwWKx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", "text/plain": [ "
" ] @@ -274,7 +291,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -298,7 +315,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -328,14 +345,14 @@ "\n", "Output_format: Your output should be in the following json format, satisfying the json syntax:\n", "\n", - "{{\n", + "{\n", "\"reasoning\": ,\n", "\"answer\": ,\n", - "\"suggestion\": {{\n", + "\"suggestion\": {\n", " : ,\n", " : ,\n", - "}}\n", - "}}\n", + "}\n", + "}\n", "\n", "In \"reasoning\", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result.\n", "\n", @@ -353,20 +370,20 @@ "You need to change the of the variables in #Variables to improve the output in accordance to #Feedback.\n", "\n", "#Code\n", - "eval84 = eval(lst=lst0, __code=__code1)\n", - "eval85 = eval(lst=lst1, __code=__code1)\n", - "eval86 = eval(lst=lst2, __code=__code1)\n", - "eval87 = eval(lst=lst3, __code=__code1)\n", - "eq0 = eq(x=eval84, y=list0)\n", - "eq1 = eq(x=eval85, y=list1)\n", - "eq2 = eq(x=eval86, y=list2)\n", - "eq3 = eq(x=eval87, y=list3)\n", + "eval90 = eval(lst=lst0, __code=__code1)\n", + "eval91 = eval(lst=lst1, __code=__code1)\n", + "eval92 = eval(lst=lst2, __code=__code1)\n", + "eval93 = eval(lst=lst3, __code=__code1)\n", + "eq0 = eq(x=eval90, y=list0)\n", + "eq1 = eq(x=eval91, y=list1)\n", + "eq2 = eq(x=eval92, y=list2)\n", + "eq3 = eq(x=eval93, y=list3)\n", "concat1 = concat(args_0=eq0, args_1=eq1, args_2=eq2, args_3=eq3)\n", "\n", "#Documentation\n", "[eval] This operator eval(__code, *args, **kwargs) evaluates the code block, where __code is the code (str) and *args and **kwargs are the arguments of the function. The output is the result of the evaluation, i.e., __code(*args, **kwargs).\n", - "[eq] This is an eq operator of x and y. .\n", - "[concat] Concatenate the items into a single string .\n", + "[eq] This is an eq operator of x and y.\n", + "[concat] Concatenate the items into a single string\n", "\n", "#Variables\n", "(code) __code1:def strange_sort_list(lst):\n", @@ -385,18 +402,18 @@ "#Inputs\n", "(list) lst1=[5, 5, 5, 5]\n", "(list) lst2=[]\n", - "(list) lst3=[9, 8, 7, 6, 5, 4]\n", "(list) lst0=[1, 2, 3, 4]\n", + "(list) lst3=[9, 8, 7, 6, 5, 4]\n", "(list) list1=[5, 5, 5, 5]\n", "(list) list2=[]\n", - "(list) list3=[4, 9, 5, 8, 6, 7]\n", "(list) list0=[1, 4, 2, 3]\n", + "(list) list3=[4, 9, 5, 8, 6, 7]\n", "\n", "#Others\n", - "(list) eval85=[5, 5, 5, 5]\n", - "(list) eval86=[]\n", - "(list) eval87=[4, 5, 6, 7, 8, 9]\n", - "(list) eval84=[1, 2, 3, 4]\n", + "(list) eval91=[5, 5, 5, 5]\n", + "(list) eval92=[]\n", + "(list) eval90=[1, 2, 3, 4]\n", + "(list) eval93=[4, 5, 6, 7, 8, 9]\n", "(bool) eq0=False\n", "(bool) eq1=True\n", "(bool) eq2=True\n", @@ -423,13 +440,23 @@ "\n", "LLM response:\n", " {\n", - " \"reasoning\": \"The feedback indicates that the eval functions are used to generate sorted lists through a given sort function defined in __code1. The eq functions compare the output of these sort functions with predefined lists (list0, list1, list2, list3). The tests for lst0 and lst3 have failed, indicating that the output from the strange_sort_list function did not match the expected pattern given in list0 and list3. From the documentation and the given list patterns, 'strange_sort_list' should alternate between the smallest and largest remaining elements in the list, but the function in __code1 currently only sorts the list in ascending order. Thus, lst0 and lst3 are not sorting correctly into their expected 'strange' order.\",\n", - " \"answer\": \"Change __code1 to implement the strange sorting pattern by alternating between selecting minimum and maximum elements.\",\n", - " \"suggestion\": {\n", - " \"__code1\": \"def strange_sort_list(lst):\\n lst = sorted(lst)\\n result = []\\n while lst:\\n result.append(lst.pop(0)) # append and remove the first (minimum)\\n if lst: # check to avoid popping from an empty list\\n result.append(lst.pop()) # append and remove the last (maximum)\\n return result\\n\"\n", - " }\n", + "\"reasoning\": \"The #Instruction requires us to modify the values in #Variables, specifically the function __code1, to improve the output according to the feedback. According to #Feedback, test cases 0 and 3 failed, while test cases 1 and 2 passed. The current definition of strange_sort_list only sorts the list in ascending order, which is not sufficient for the 'strange order' specified. The 'strange order' is defined as starting with the minimum, then the maximum of the remaining, then the next minimum, and so forth. Therefore, we need to modify the function strange_sort_list(lst) to implement this logic. \\n\\nThe correct transformation should alternate between taking the smallest and largest remaining values in the list until the list is exhausted. This adjustment will ensure lists such as lst0 and lst3 are correctly transformed to match list0 and list3, respectively.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"__code1\": \"def strange_sort_list(lst):\\n '''\\n Given list of integers, return list in strange order.\\n Strange sorting, is when you start with the minimum value,\\n then maximum of the remaining integers, then minimum and so on.\\n '''\\n lst = sorted(lst)\\n result = []\\n while lst:\\n result.append(lst.pop(0)) # take min\\n if lst:\\n result.append(lst.pop(-1)) # take max\\n return result\"\n", + "}\n", "}\n" ] + }, + { + "data": { + "text/plain": [ + "{: \"def strange_sort_list(lst):\\n '''\\n Given list of integers, return list in strange order.\\n Strange sorting, is when you start with the minimum value,\\n then maximum of the remaining integers, then minimum and so on.\\n '''\\n lst = sorted(lst)\\n result = []\\n while lst:\\n result.append(lst.pop(0)) # take min\\n if lst:\\n result.append(lst.pop(-1)) # take max\\n return result\"}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -476,6 +503,343 @@ "optimizer.backward(batched_outputs, batched_feedback.data)\n", "optimizer.step(verbose=True)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the functions in `opto.trainer` to perform Batching\n", + "\n", + "In the earlier examples, we wrote our own design patterns for accomplishing batch optimization. However, Trace provides the `MiniBatchAlgorithm` to accomplish this automatically.\n", + "Let us see how the abstractions in `opto.trainer` allow us to scale up optimization, for example, doing minibatch optimization on the GSM 8K Dataset, which is a dataset of math word problems." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "import datasets\n", + "import numpy as np\n", + "\n", + "train_dataset = datasets.load_dataset('openai/gsm8k', 'main')['train'][:10]\n", + "train_dataset = dict(inputs=train_dataset['question'], infos=train_dataset['answer'])\n", + "test_dataset = train_dataset\n", + "\n", + "# set seed\n", + "seed = 42\n", + "num_epochs = 1\n", + "batch_size = 2\n", + "eval_frequency = -1\n", + "num_threads = 3\n", + "verbose = True\n", + "\n", + "np.random.seed(seed)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define the `Learner` agent which is a student LLM with a trainable system prompt. Trace will use a generative optimizer to tune the system prompt. Trace provides also a class for LLM-as-Judge called `VerbalJudgeGuide` that uses a Teacher LLM to provide rich feedbacks to the student LLM. " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "from opto import trace\n", + "from opto.utils.llm import LLM\n", + "from opto.optimizers import OptoPrime\n", + "from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm\n", + "from opto.trainer.loggers import TensorboardLogger\n", + "from opto.trainer.guide import VerbalJudgeGuide\n", + "from typing import Any\n", + "\n", + "@trace.model\n", + "class Learner:\n", + " \"\"\" A basic LLM agent. \"\"\"\n", + "\n", + " def __init__(self, system_prompt: str = \"You're a helpful agent\",\n", + " user_prompt_template: str = \"Query: {message}\",\n", + " llm: LLM = None):\n", + " self.system_prompt = trace.node(system_prompt, trainable=True)\n", + " self.user_prompt_template = trace.node(user_prompt_template)\n", + " self.llm = llm or LLM()\n", + "\n", + " @trace.bundle()\n", + " def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str:\n", + " \"\"\"Call the LLM model.\n", + "\n", + " Args:\n", + " system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem.\n", + " user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message).\n", + " message: the input to the agent. It can be a query, a task, a code, etc.\n", + " Returns:\n", + " The response from the agent.\n", + " \"\"\"\n", + "\n", + " if '{message}' not in user_prompt_template:\n", + " raise ValueError(\"user_prompt_template must contain '{message}'\")\n", + "\n", + " response = self.llm(\n", + " messages=[{\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt_template.format(message=message)}]\n", + " )\n", + " return response.choices[0].message.content\n", + "\n", + " def forward(self, message: Any) -> Any:\n", + " \"\"\" Forward pass of the agent. \"\"\"\n", + " return self.model(self.system_prompt, self.user_prompt_template, message)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we use the `MiniBatchAlgorithm` as the trainer to sample batches from the GSM8K dataset, run the student model on the samples, gather feedback from the teacher model, and present the resulting traced graph to the optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:06<00:00, 3.12s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The #Instruction asks us to change the values of the variables in #Variables to improve the output according to #Feedback. The #Feedback section provides the analysis of the answers generated for each query. Both answers for the queries (regarding Alexis and Weng) are correct, as indicated by the statement 'Correct [TERMINATE]'. The #Output shows that the responses generated for each model (Learner.model0 and Learner.model1) are logical and correct given the input prompts. Therefore, there are no errors in the current setup, and no changes are needed in the variables.\",\n", + "\"answer\": \"TERMINATE\",\n", + "\"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 1): 100%|██████████| 10/10 [00:22<00:00, 2.30s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 1] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 1\n", + "[Step 1] Instantaneous train score: 1.0\n", + "[Step 1] Average train score: 1.0\n", + "[Step 1] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:09<00:00, 4.65s/it]\n", + "/home/aswaminathan/miniconda3/envs/trace/lib/python3.9/copy.py:263: RuntimeWarning: coroutine 'main' was never awaited\n", + " args = (deepcopy(arg, memo) for arg in args)\n", + "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction asks us to change the value of variables if necessary to improve the output based on the feedback provided. In this instance, the feedback for both outputs (ID [0] and ID [1]) states 'Correct' and suggests termination, which indicates that the outputs match the expected results. The variables in the code that we have control over are used to set up prompts for an LLM model to process. The feedback shows the model's output correctly answers the questions based on the inputs, matching the expected correct answers outlined in the feedback. Therefore, no changes to the variables are necessary as the task is operating as intended.\",\n", + "\"answer\": \"TERMINATE\",\n", + "\"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [00:18<00:00, 1.88s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 2\n", + "[Step 2] Instantaneous train score: 1.0\n", + "[Step 2] Average train score: 1.0\n", + "[Step 2] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:04<00:00, 2.46s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The #Instruction asks us to adjust the #Variables to improve the output based on #Feedback. The feedback suggests that the answers provided by the models are correct for both IDs. The output of both Learner.model25 and Learner.model24 correctly represents the calculation processes needed to answer the given queries. As the feedback indicates '[TERMINATE]', it means the current outputs are satisfactory, and no changes to the #Variables are necessary.\",\n", + " \"answer\": \"TERMINATE\"\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 3): 100%|██████████| 10/10 [00:20<00:00, 2.05s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 3] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 3\n", + "[Step 3] Instantaneous train score: 1.0\n", + "[Step 3] Average train score: 1.0\n", + "[Step 3] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:08<00:00, 4.16s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The #Instruction requires us to change the values in #Variables to improve the output. However, based on #Feedback, both IDs in the #Outputs are correctly calculated according to the logic specified in #Documentation and supported by expert feedback. Therefore, no changes are needed to improve the outputs, as they already match the expected results provided in the feedback.\",\n", + "\"answer\": \"Both outputs are correct as per the feedback.\",\n", + "\"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [00:19<00:00, 1.91s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 4\n", + "[Step 4] Instantaneous train score: 1.0\n", + "[Step 4] Average train score: 1.0\n", + "[Step 4] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 2): 100%|██████████| 2/2 [00:05<00:00, 2.63s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The #Instruction requires adjusting the value of the variable in #Variables to improve the output based on #Feedback. In this scenario, the feedback has been provided for both outputs (ID [0] and ID [1]) as correct, with an explicit [TERMINATE] instruction from the expert feedback, indicating that no changes are needed for the variable's value, as the outputs align perfectly with the expected answers. The current settings in #Variables, #Inputs, and #Others, including the prompts and message, are correctly leading to the generation of accurate answers to the queries, both for Julie's reading task and Albert's pizza consumption problem.\",\n", + "\"answer\": \"TERMINATE\",\n", + "\"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 5): 100%|██████████| 10/10 [00:17<00:00, 1.76s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 5] \u001b[92mAverage test score: 1.0\u001b[0m\n", + "Epoch: 0. Iteration: 5\n", + "[Step 5] Instantaneous train score: 1.0\n", + "[Step 5] Average train score: 1.0\n", + "[Step 5] \u001b[91mParameter: str:20: You're a helpful agent\u001b[0m\n", + "FINISHED TRAINING\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "agent = Learner(llm=LLM())\n", + "guide = VerbalJudgeGuide(llm=LLM())\n", + "optimizer = OptoPrime(agent.parameters(), llm=LLM())\n", + "logger = TensorboardLogger(verbose=True)\n", + "\n", + "alg = MinibatchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger)\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "import asyncio\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING\")\n", + " alg.train(guide,\n", + " train_dataset,\n", + " num_epochs=num_epochs,\n", + " batch_size=batch_size,\n", + " eval_frequency=eval_frequency,\n", + " test_dataset=test_dataset,\n", + " num_threads=num_threads,\n", + " verbose='output')\n", + " print(\"FINISHED TRAINING\")\n", + " \n", + "asyncio.run(wrapper())" + ] } ], "metadata": { @@ -494,7 +858,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/tutorials/optimization_tutorial.ipynb b/docs/tutorials/optimization_tutorial.ipynb index 78511199..0cf4ca1f 100644 --- a/docs/tutorials/optimization_tutorial.ipynb +++ b/docs/tutorials/optimization_tutorial.ipynb @@ -17,16 +17,75 @@ }, "outputs": [], "source": [ - "%pip install trace-opt" + "%pip install trace-opt ipywidgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below provides a way to specify your API_KEY for calling LLMs using LiteLLM as part of this tutorial notebook. Alternatively, provide the keys by setting environment variables or loading LiteLLM config files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import opto\n", "from opto.trace import bundle, node\n", "from opto.optimizers import OptoPrime\n", "from opto.trace.nodes import GRAPH\n", @@ -74,7 +133,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -173,8 +232,6 @@ } ], "source": [ - "import autogen\n", - "\n", "# One-step optimization example\n", "x = node(-1.0, trainable=True)\n", "optimizer = OptoPrime([x])\n", @@ -444,7 +501,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -465,7 +522,7 @@ "source": [ "# A small example of how to include constraints on parameters\n", "GRAPH.clear()\n", - "x = node(-1.0, trainable=True, constraint=\"The value should be greater than 2.0\")\n", + "x = node(-1.0, trainable=True, description=\"The value should be greater than 2.0\")\n", "optimizer = OptoPrime([x])\n", "\n", "history = [x.data]\n", @@ -956,7 +1013,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.15" + "version": "3.9.23" } }, "nbformat": 4, diff --git a/docs/tutorials/trainers.ipynb b/docs/tutorials/trainers.ipynb new file mode 100644 index 00000000..84f64fa8 --- /dev/null +++ b/docs/tutorials/trainers.ipynb @@ -0,0 +1,2860 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using `opto.trainer` algorithms for scaling up generative optimization\n", + "\n", + "This tutorial walks you through the different algorithms that have been built on top of the generative optimizers in Trace.\n", + "The `minibatch` tutorial already showed one specific use-case: `MiniBatchAlgorithm` that takes an agent, dataset and opto optimizer as inputs and outputs an optimized agent. \n", + "In fact, all of the algorithms in `opto.trainer` obey this basic input-output mapping; they all use the opto optimizers to propose candidate parameters, but utilize different search procedures on top of that to refine the optimized agent.\n", + "\n", + "We will use the [HardMath dataset](https://huggingface.co/datasets/xuanfeiren/math_hard_gemini) in this tutorial to illustrate the various algorithms in `opto.trainer`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%pip install trace-opt ipywidgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below provides a way to specify your API_KEY for calling LLMs using LiteLLM as part of this tutorial notebook. Alternatively, provide the keys by setting environment variables or loading LiteLLM config files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "# Function to save the environment variable and API key\n", + "def save_env_variable(env_name, api_key):\n", + " # Validate inputs\n", + " if not env_name.strip():\n", + " print(\"⚠️ Environment variable name cannot be empty.\")\n", + " return\n", + " if not api_key.strip():\n", + " print(\"⚠️ API key cannot be empty.\")\n", + " return\n", + " \n", + " # Store the API key as an environment variable\n", + " os.environ[env_name] = api_key\n", + " globals()[env_name] = api_key # Set it as a global variable\n", + " print(f\"✅ API key has been set for environment variable: {env_name}\")\n", + "\n", + "# Create the input widgets\n", + "env_name_input = widgets.Text(\n", + " value=\"OPENAI_API_KEY\", # Default value\n", + " description=\"Env Name:\",\n", + " placeholder=\"Enter env variable name (e.g., MY_API_KEY)\",\n", + ")\n", + "\n", + "api_key_input = widgets.Password(\n", + " description=\"API Key:\",\n", + " placeholder=\"Enter your API key\",\n", + ")\n", + "\n", + "# Create the button to submit the inputs\n", + "submit_button = widgets.Button(description=\"Set API Key\")\n", + "\n", + "# Display the widgets\n", + "display(env_name_input, api_key_input, submit_button)\n", + "\n", + "# Callback function for the button click\n", + "def on_button_click(b):\n", + " env_name = env_name_input.value\n", + " api_key = api_key_input.value\n", + " save_env_variable(env_name, api_key)\n", + "\n", + "# Attach the callback to the button\n", + "submit_button.on_click(on_button_click)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We load the dataset and define a `Guide` (i.e. LLM-as-Judge) that can provide feedback for answers to questions in the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/aswaminathan/miniconda3/envs/trace/lib/python3.9/site-packages/flaml/__init__.py:20: UserWarning: flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.\n", + " warnings.warn(\"flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training samples: 20\n", + "Validation samples: 20\n", + "Test samples: 10\n" + ] + } + ], + "source": [ + "import datasets\n", + "import numpy as np\n", + "from typing import Any, Tuple\n", + "from opto.trainer.guide import AutoGuide\n", + "from opto.utils.llm import LLM\n", + "\n", + "# Set random seed\n", + "np.random.seed(42)\n", + "\n", + "math_data = datasets.load_dataset('xuanfeiren/math_hard_gemini')\n", + "train_data = math_data['train'].select(\n", + " range(10, 30)\n", + " )\n", + "validate_data = train_data\n", + "test_data = math_data['test'].select(range(10))\n", + "\n", + "# Format data for trainer\n", + "train_dataset = {'inputs': train_data['problem'], 'infos': train_data['solution']}\n", + "validate_dataset = {'inputs': validate_data['problem'], 'infos': validate_data['solution']}\n", + "test_dataset = {'inputs': test_data['problem'], 'infos': test_data['solution']}\n", + "\n", + "# Log dataset sizes\n", + "print(f\"Training samples: {len(train_dataset['inputs'])}\")\n", + "print(f\"Validation samples: {len(validate_dataset['inputs'])}\")\n", + "print(f\"Test samples: {len(test_dataset['inputs'])}\")\n", + "\n", + "\n", + "class TeacherGuide(AutoGuide):\n", + " \"\"\"Guide that uses LLM to judge answers and provide feedback.\"\"\"\n", + " \n", + " def __init__(self, model: str = \"gpt-4o-mini\"):\n", + " \"\"\"Initialize the teacher guide.\n", + " \n", + " Args:\n", + " model: The LLM model to use for evaluation\n", + " \"\"\"\n", + " super().__init__()\n", + " self.guide_llm = LLM(model=model)\n", + " self.system_prompt = \"You are an expert math teacher evaluating student answers.\"\n", + " self.judge_prompt_template = (\n", + " \"Carefully review the following three distinct sections:\\n\\n\"\n", + " \"SECTION 1: The Math Problem\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{query}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"SECTION 2: The Student's Full Answer\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{response}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"SECTION 3: The Official Correct Answer\\n\"\n", + " \"----------------------------\\n\"\n", + " \"{reference}\\n\"\n", + " \"----------------------------\\n\\n\"\n", + " \"INSTRUCTIONS FOR JUDGING:\\n\"\n", + " \"1. Your primary task is to compare the student's **final numerical result** (or final conclusion if no number is present) from SECTION 2 with the **Official Correct Answer** provided in SECTION 3.\\n\"\n", + " \"2. When evaluating SECTION 2 (Student's Full Answer), focus SOLELY on the **final answer part** of the student's response. Ignore all intermediate steps, reasoning, or explanations for the correctness check unless the problem specifically asks for reasoning as the final answer.\\n\"\n", + " \"3. Determine if the student's **final answer** is equivalent to the **Official Correct Answer**.\\n\\n\"\n", + " \"RESPONSE FORMAT:\\n\"\n", + " \"- If the student's final answer (from SECTION 2) IS equivalent to the Official Correct Answer (from SECTION 3), respond ONLY with the exact phrase: 'Correct [TERMINATE]'\\n\"\n", + " \"- If the student's final answer IS NOT equivalent, respond ONLY with specific and actionable feedback. The feedback should clearly explain the error in the student's final answer and guide them on how to arrive at the Official Correct Answer.\"\n", + " )\n", + "\n", + " def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]:\n", + " \"\"\"Get feedback on a student response.\n", + " \n", + " Args:\n", + " task: The original math problem\n", + " response: The student's answer\n", + " info: The reference/correct answer\n", + " **kwargs: Additional arguments\n", + " \n", + " Returns:\n", + " Tuple of (score, feedback_text)\n", + " \"\"\"\n", + " user_prompt = self.judge_prompt_template.format(\n", + " query=task,\n", + " response=response,\n", + " reference=info\n", + " )\n", + "\n", + " messages = [\n", + " {\"role\": \"system\", \"content\": self.system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ]\n", + "\n", + " llm_response = self.guide_llm(messages=messages)\n", + " feedback_text = llm_response.choices[0].message.content\n", + "\n", + " if 'Correct [TERMINATE]' in feedback_text:\n", + " return 1.0, \"Correct.\"\n", + " else:\n", + " return 0.0, f\"Incorrect. Feedback: {feedback_text}\"\n", + " \n", + " def metric(self, task: str, content: str, info: Any, **kwargs) -> float:\n", + " \"\"\"Calculate the metric score for an answer.\n", + " \n", + " Args:\n", + " task: The original math problem\n", + " content: The student's answer\n", + " info: The reference/correct answer\n", + " **kwargs: Additional arguments\n", + " \n", + " Returns:\n", + " Score (0.0 or 1.0)\n", + " \"\"\"\n", + " score, _ = self.get_feedback(task, content, info, **kwargs)\n", + " return score" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define the `Learner` agent which is a student LLM with a trainable `system prompt` and a trainable `user prompt template`. Trace will use a generative optimizer to tune these prompts." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from opto import trace\n", + "from opto.optimizers import OptoPrime\n", + "from opto.optimizers.utils import print_color\n", + "from opto.trace.modules import Module\n", + "from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm\n", + "from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm\n", + "from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm\n", + "\n", + "\n", + "@trace.model\n", + "class Learner(Module):\n", + " \"\"\"A basic LLM Agent for solving math problems.\"\"\"\n", + " \n", + " def __init__(self, \n", + " system_prompt: str = \"You're a helpful agent answering math problems.\",\n", + " user_prompt_template: str = \"Solve the following math problem step-by-step: {message}\",\n", + " llm: LLM = None):\n", + " \"\"\"Initialize the learner agent.\n", + " \n", + " Args:\n", + " system_prompt: System prompt to guide LLM behavior\n", + " user_prompt_template: Template for formatting user messages\n", + " llm: LLM instance to use for generation (defaults to gpt-3.5-turbo)\n", + " \"\"\"\n", + " super().__init__()\n", + " self.system_prompt = trace.node(system_prompt, trainable=True)\n", + " self.user_prompt_template = trace.node(user_prompt_template, trainable=True)\n", + " self.llm = llm or LLM(model=\"gpt-3.5-turbo\")\n", + "\n", + " @trace.bundle()\n", + " def call_llm(self, system_prompt: str, user_prompt: str) -> str:\n", + " \"\"\"Call LLM model with the given prompts.\n", + " \n", + " Args:\n", + " system_prompt: The system prompt\n", + " user_prompt: The user prompt\n", + " \n", + " Returns:\n", + " The LLM response content\n", + " \"\"\"\n", + " response = self.llm(\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": system_prompt},\n", + " {\"role\": \"user\", \"content\": user_prompt}\n", + " ]\n", + " )\n", + " return response.choices[0].message.content\n", + "\n", + " def forward(self, message: Any) -> str:\n", + " \"\"\"Agent's forward pass to process a message.\n", + " \n", + " Args:\n", + " message: The input message to process\n", + " \n", + " Returns:\n", + " The generated response\n", + " \"\"\" \n", + " user_prompt = self.user_prompt_template.format(message=message)\n", + " return self.call_llm(self.system_prompt, user_prompt)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We initialize all the components: the agent using the student LLM, the guide using the teacher LLM, and the optimizer using an LLM as a generative optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "student_llm = LLM()\n", + "agent = Learner(llm=student_llm)\n", + "\n", + "train_guide = TeacherGuide()\n", + "validate_guide = TeacherGuide()\n", + "\n", + "optimizer = OptoPrime(agent.parameters())\n", + "\n", + "from opto.trainer.loggers import DefaultLogger\n", + "class SimpleLogger(DefaultLogger):\n", + " \"\"\"Simplified logger that only shows important metrics.\"\"\"\n", + " \n", + " def log(self, name: str, data: Any, step: int, **kwargs):\n", + " \"\"\"Log only specific metrics to reduce output clutter.\n", + " \n", + " Args:\n", + " name: The name of the metric\n", + " data: The metric value\n", + " step: The current step\n", + " **kwargs: Additional logging arguments\n", + " \"\"\"\n", + " important_metrics = [\n", + " 'Average train score',\n", + " 'Average test score',\n", + " 'Validation score'\n", + " ]\n", + " \n", + " if name in important_metrics or 'Parameter' in name:\n", + " super().log(name, data, step, **kwargs)\n", + "\n", + "logger = SimpleLogger()\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "import asyncio\n", + "\n", + "train_params = {\n", + " \"guide\": train_guide,\n", + " \"train_dataset\": train_dataset,\n", + " \"num_epochs\": 1,\n", + " \"num_threads\": 5,\n", + " \"batch_size\": 5,\n", + " \"test_dataset\": test_dataset,\n", + " \"validate_dataset\": validate_dataset,\n", + " \"validate_guide\": validate_guide,\n", + " \"eval_frequency\": 2,\n", + " \"log_frequency\": 2,\n", + " #for Basic Search\n", + " \"num_proposals\": 2,\n", + " #for Beam Search\n", + " \"validation_dataset_size\": 5,\n", + " \"beam_width\": 3,\n", + " \"max_depth\": 4,\n", + " \"max_history_size\": 2,\n", + " #for UCB Search\n", + " \"num_search_iterations\": 3,\n", + " \"train_batch_size\": 5,\n", + " \"evaluation_batch_size\": 5,\n", + " \"max_buffer_size\": 3,\n", + " \"ucb_exploration_factor\": 1.0\n", + " }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we will go through each of the algorithms in `opto.trainer`. Each algorithm will run the student model on the train dataset, gather feedback from the teacher model, present the resulting traced graph to the optimizer, and then perform specific post-processing throughout each training epoch." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING MINIBATCH\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 0): 100%|██████████| 10/10 [00:52<00:00, 5.26s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 0] \u001b[92mAverage test score: 0.4\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.05s/it]\n", + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:52<00:00, 10.40s/it]\n", + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [00:50<00:00, 5.06s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 2\n", + "[Step 2] Average train score: 0.2\n", + "[Step 2] \u001b[91mParameter: str:0: You're a helpful agent assisting with thorough and complete mathematical problem analysis, ensuring all steps are accurately validated.\u001b[0m\n", + "[Step 2] \u001b[91mParameter: str:1: Carefully process each subcomponent of the following problem: {message} Methodically ensure completeness in probability calculations, permutations, customizable solutions, and systematic explorations of possible outcomes.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:49<00:00, 9.88s/it]\n", + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:28<00:00, 5.64s/it]\n", + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [01:01<00:00, 6.10s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 4\n", + "[Step 4] Average train score: 0.2\n", + "[Step 4] \u001b[91mParameter: str:0: Accurate precision ensuring number coating and span impart cataloguing upon probability, permutation, solution synthesis, and structured exploration\u001b[0m\n", + "[Step 4] \u001b[91mParameter: str:1: Diligently analyze each part facet of the offering issue: {message} carefuly ascertain completion in probability computation, permutation exercise, customizable provides solution, and scheme sized explorable outcomes.\u001b[0m\n", + "FINISHED TRAINING MINIBATCH\n", + "Final score: 0.2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = MinibatchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING MINIBATCH\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING MINIBATCH\")\n", + " print(\"Final score: \", final_score)\n", + "\n", + "asyncio.run(wrapper())" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING BASIC SEARCH\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 0): 100%|██████████| 10/10 [01:06<00:00, 6.63s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 0] \u001b[92mAverage test score: 0.2\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:32<00:00, 6.52s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:12<00:00, 6.32s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [00:22<00:00, 1.12s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:40<00:00, 5.00s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:16<00:00, 6.82s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 0] \u001b[92mValidation score: 0.05\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:38<00:00, 7.76s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:15<00:00, 7.88s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [02:22<00:00, 7.14s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:21<00:00, 4.05s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 1] \u001b[92mValidation score: 0.15\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 2): 100%|██████████| 10/10 [01:03<00:00, 6.32s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mAverage test score: 0.2\u001b[0m\n", + "Epoch: 0. Iteration: 2\n", + "[Step 2] Average train score: 0.1\n", + "[Step 2] \u001b[91mParameter: str:0: Critically examine and describe each step of the problem-solving process, ensuring thorough precision in applying combinatorial logic, sequence conversions, and probability distributions within complex scenarios such as probability computation, permutation exercise, solution synthesis, and exploration of structured outcomes.\u001b[0m\n", + "[Step 2] \u001b[91mParameter: str:1: Evaluate each component in detail for the given problem situation: {message} employing strategic reasoning to ascertain completion in logical computation, solving exercises through permutations, offering customizable solutions, and unveiling outcomes of scenario explorations.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:41<00:00, 8.34s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:21<00:00, 10.85s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:41<00:00, 5.08s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 2] \u001b[92mValidation score: 0.15\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:40<00:00, 8.13s/it]\n", + "Generating 2 proposals: 100%|██████████| 2/2 [00:11<00:00, 5.89s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:24<00:00, 4.24s/it]\n", + "Validating proposals: 100%|██████████| 20/20 [01:25<00:00, 4.25s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 3] \u001b[92mValidation score: 0.15\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating agent (iteration 4): 100%|██████████| 10/10 [00:45<00:00, 4.52s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Step 4] \u001b[92mAverage test score: 0.3\u001b[0m\n", + "Epoch: 0. Iteration: 4\n", + "[Step 4] Average train score: 0.15000000000000002\n", + "[Step 4] \u001b[91mParameter: str:0: Critically examine and describe each step of the problem-solving process, ensuring thorough precision in applying combinatorial logic, sequence conversions, and probability distributions within complex scenarios such as probability computation, permutation exercise, solution synthesis, and exploration of structured outcomes.\u001b[0m\n", + "[Step 4] \u001b[91mParameter: str:1: Evaluate each component in detail for the given problem situation: {message} employing strategic reasoning to ascertain completion in logical computation, solving exercises through permutations, offering customizable solutions, and unveiling outcomes of scenario explorations.\u001b[0m\n", + "FINISHED TRAINING BASIC SEARCH\n", + "Final score: 0.3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = BasicSearchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING BASIC SEARCH\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING BASIC SEARCH\")\n", + " print(\"Final score: \", final_score)\n", + " \n", + "asyncio.run(wrapper())" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING BEAM SEARCH\n", + "\u001b[94mRunning BeamsearchAlgorithm with beam_width=3, max_depth=4\u001b[0m\n", + "\u001b[94mUsing validation_dataset_size=5 for intermediate evaluations\u001b[0m\n", + "\u001b[94m\n", + "===== Evaluating Initial Parameters =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating initial parameters on test set: 100%|██████████| 10/10 [00:41<00:00, 4.18s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[93mInitial test score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 1/4 with 1 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 1\u001b[0m\n", + "\u001b[93mProcessing beam 1/1\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:23<00:00, 4.70s/it]\n", + "Generating 2 proposals for beam 1: 50%|█████ | 1/2 [00:09<00:09, 9.32s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The feedback provided indicates issues with the outcomes computed in the code for some problem instances. Here's a breakdown:\\n1. ID[0]: The student's calculated answer was off due to an incorrect count of distinct collections of consonants. They provided 87 when the correct count is 72. This suggests re-evaluating how the consonants are grouped without double-counting. The construction of possible usage scenarios needs correction to prevent overlap and ensure unique contributions.\\n2. ID[1] was correct, so no changes are needed for this problem.\\n3. ID[2]: The student's understanding of permutations and probabilities based on the lattice was incorrect. They concluded with a probability of 1/16, but the correct symmetry of movements on the lattice results in a probability of 1/4. This indicates a need to consider the even distribution across potential endpoints on the lattice, using symmetry to realize each endpoint is equally probable.\\n4. ID[3] was correct, so no changes are needed.\\n5. ID[4]: The student's calculations were more complex than necessary, leading to an incorrect conclusion of 166167 when the answer should be 5. The problem requires a simpler combinatorial logic by recognizing dimension fitting and using basic probability, resulting in a sum of numerator and denominator equating to 5.\\n\\nTo implement the feedback correctly, the problems need to be approached with a clearer fundamental understanding of combinatorics, symmetry, and probability logic.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"Consider simplifying the logic for each distinct problem, focusing on symmetry and leveraging basic combinatorial approaches to arrive at official solutions efficiently.\",\n", + " \"str1\": \"Re-evaluate vowel and consonant combinations, account for symmetry correctly on lattice problems, and simplify the dimensions's fitting logic to reach conclusions aligned with official answers.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1: 100%|██████████| 2/2 [00:09<00:00, 4.83s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The #Instruction requires us to adjust the value of variables in #Variables section to improve the outputs based on the #Feedback given. There are 5 different task outputs in #Outputs, and their correctness is indicated in the #Feedback. For ID [0] and ID [2], the feedback states that the student's answers are incorrect because of miscalculations in combinations and probabilities respectively. Similarly, ID [4] indicates an incorrect solution due to overcomplication, whereas IDs [1] and [3] are marked as correct. The primary variables influencing those outputs are 'str0' and 'str1' which are used in the prompts. Given the feedback, we should refine the calculation logic or reformulate the problem addressing prompts through a corrected detailed and clear explanation. In particular, ID [0] requires recalculating distinct collections, ID [2] involves improving probability distribution calculations, and ID [4] involves refining the method to understand the combinatorial setup. Thus, an updated 'str0' and 'str1' that better frames the problems for correct consequence inference in respective calculations is suggested. This redesign would align more closely with correct reasoning directives, resolving calculation errors without explicit instruction knowledge beyond what's provided.\",\n", + " \"answer\": \"\", \n", + " \"suggestion\": {\n", + " \"str0\": \"Evaluate detailed logic approaches focusing on recognizing constraints properly in permutation or probability setups, ensuring combinatorial approaches align with expected constraints effectively in complex scenarios. Reassess frame scenarios for multi-step conclusion tactics in either general problem solving or result synthesis.\",\n", + " \"str1\": \"Examine stepwise construction ensuring solutions with logical reasoning intact from raw deduction to systematic analytics. Revise cases with particular attention to parameter distinctions, securing robust resolution across permutation or probability contexts within logistical boundaries.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/3: 100%|██████████| 5/5 [00:17<00:00, 3.48s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/3: 100%|██████████| 5/5 [00:24<00:00, 4.96s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/3: 100%|██████████| 5/5 [00:23<00:00, 4.74s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.6000\u001b[0m\n", + "\u001b[92mKeeping all 3 candidates as num_candidates <= beam_width. Scores: ['0.0000', '0.0000', '0.6000']\u001b[0m\n", + "\u001b[92mDepth 1 - Best validation score: 0.6000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 2/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 2\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:24<00:00, 4.80s/it]\n", + "Generating 2 proposals for beam 1: 100%|██████████| 2/2 [00:09<00:00, 4.51s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction requires adjusting the given variable values to improve the output by aligning it with the feedback explanations, which indicate specific answers. The code involves concatenating results from different calls to an LLM model. The variables str0 and str1 seem to contain information used to guide the models but do not directly influence the output-related math problems according to feedback. Each output from Learner.call_llm corresponds to a different math problem with specific expected answers:\\n\\n1. **Problem on Coordinate Plane (format290):** Expected to result in `m + n` for the probability expressed as `m/n`. Requires calculating paths and probabilities reaching `(2,2)` in 6 or fewer steps.\\n\\n2. **Locker Problem (format291):** Needs an explicit pattern recognition or calculation to find that locker number 342 is the last opened.\\n\\n3. **Handshake Problem (format292):** Requires solving an equation to find the minimum handshakes for the coach; targeted response is `k = 5`.\\n\\n4. **Distribution of Cousins (format293):** Focuses on combinatorial arrangements resulting in 15 distinct possibilities.\\n\\n5. **Letters in Bag (format294):** Entails selecting from indistinguishable vowels and consonants; expected answer is 72 distinct groupings.\\n\\nImproving the output requires entering these specific answers as potential checks or calculations (not modifying descriptions) for refining model interactions.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"Ensure model outputs are calculated or aligned with problem solutions to provide final numerical answers, adjusting user prompt if necessary.\",\n", + " \"str1\": \"Consider cross-verifying correct computations for expected outcomes if descriptions affect logic processes in model response.\"\n", + "}\n", + "}\n", + "LLM response:\n", + " {\n", + " \"reasoning\": \"The #Instruction is asking for a change in variable values located in #Variables based on #Feedback to arrive at the desired output. The #Feedback indicates that the provided responses do not yield the correct final numerical answers for the specific mathematical problems described. The #Feedback for each ID denotes issues related to lack of computation towards the expected solutions. The code utilizes string formatting and LLM calling to concatenate messages and employ model outputs into a batchify function, aiming to find specific results for combinatoric and mathematical problems given in the messages. By understanding the connections between mathematical concepts like combinations, symmetry, and fitting logic, and the expected outputs, it becomes clear that we need to tailor the provided input strings related to str0 and str1 to be more specific to the calculations required by the feedback given in #Others.\",\n", + " \"answer\": \"Adjust the contents of str0 and str1 to focus directly on the calculations needed for each problem to swing towards specific solutions highlighted in #Feedback.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Solve each problem by directly calculating distinct answers. For the object reaching (2,2) in steps, determine all ways in 4 or 6 steps. For the lockers, trace each open-close step carefully until locker 342 is the last. For gymnasts, calculate combinations where total is precisely 281, minimizing coach handshakes. For cousins, enumerate placement variants for each room combination. For letters, determine indistinguishable combinations focusing on T's, M's, and A's.\",\n", + " \"str1\": \"Focus directly on providing numeric answers by applying combinatorics, symmetry, and dimensions fitting logic specific to the distinct problem being queried, ensuring detailed, step-by-step solutions addressing the exact scenarios described.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:19<00:00, 3.90s/it]\n", + "Generating 2 proposals for beam 2: 50%|█████ | 1/2 [00:08<00:08, 8.46s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction requests to modify the #Variables to improve the output according to #Feedback. The Feedback indicates the necessity to provide a numerical answer derived through a specific process related to calculating probabilities, permutations, or identifying properties of Pascal's triangle, depending on the specific task associated with each ID. \\n\\nIn this particular scenario, the issue arises because the outputs do not reflect the Answers' expectations, which requires calculating quantitative results rather than providing general problem-solving frameworks. To generate meaningful outputs, variables `str0` and `str1` should be directly focused on the calculations and conclusions needed to solve each problem as stated in the Feedback rather than maintaining the broad guidance.\\n\\nFor example, using correct methodologies detailed in the feedback analysis:\\n- For the problem where a specific four-digit number is requested, `str1` should compute to find this number directly, focusing on going through binomial coefficients in Pascal's triangle, rather than applying a general strategy.\",\n", + "\"answer\": \"In order to fix the issues as per the feedback, the student should focus on calculating specific numerical conclusions for the problems. Example numbers to solve: for permutation/probability tasks to ensure logic completion, use probabilistic formulas directly related to the Official Correct Answers.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Calculate probabilities or permutations directly as based on details provided in #Feedback for problems involving probability or permutation involving combinatory complex problems.\",\n", + " \"str1\": \"Implement logic that derives specific numerical answers from computations and processes as guided by feedback directives, i.e., calculate probabilities or specific number identifications.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2: 100%|██████████| 2/2 [00:12<00:00, 6.06s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The feedback indicates that all responses from the Learner lack a final, numerical conclusion in their outputs, which are necessary to assess correctness against the given problems. The output lacks specificity and alignment with the official correct answers of specific problems, requiring a shift in approach to deliver concrete results. This discrepancy stems from a misalignment between the problem-solving framework, which is more general, and the need for specific numerical outputs or solutions pertinent to each problem scenario.\\n\\nThe feedback suggests more focused engagement with each problem's constraints and calculations to arrive at concrete solutions. \\n\\n- Feedback ID [0] expects a probability calculation yielding a sum of integers m+n = 67.\\n- Feedback ID [1] specifies a need to calculate a probability in a specific problem, with a final result of 931.\\n- Feedback ID [2] demands computation of a probability of selecting letters resulting in the final answer of 1/30.\\n- Feedback ID [3] involves solving a maximization problem under specified constraints with a result of 905.\\n- Feedback ID [4] requires identification of numbers from Pascal's triangle, concluding with finding the number 1001.\\n\\nFor all instances, the need is bridging between theoretical understanding and specific application with numerical outcomes.\",\n", + " \"answer\": \"The output lacks a numerical final answer across all learner instances. The correct values expected are:\\n1. 67\\n2. 931\\n3. 1/30\\n4. 905\\n5. 1001\",\n", + " \"suggestion\": {\n", + " \"str0\": \"To successfully modify the output based on learner feedback, ensure numerically precise and focused conclusions. The inputs need to adequately represent the problems and provide the necessary constraints or conditions to produce expected numeric solutions. This could include amending problem setups or data inputs that guide process flows and lead to direct calculations, ultimately converging towards the needed answer.\",\n", + " \"str1\": \"Close engagement with particular scenarios for permutations, probabilities, and combinatorial setups should be emphasized. Directly addressing problem contexts provided in messages within format and learner calls, converting theoretical methodologies into practical solutions, including using the right combination of provided inputs and necessary numeric manipulations to accurately compute the specific expected outputs. For checks, align outputs stringently to feedback expectations.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:27<00:00, 5.52s/it]\n", + "Generating 2 proposals for beam 3: 50%|█████ | 1/2 [00:12<00:12, 12.30s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction asks to change the values of variables to improve the output according to feedback.\\n\\n2. The feedback indicates the issues with each problem solution provided. Specifically:\\n- For ID [0], the probability calculation neglected the lattice's color structure. The ant can only land on point B with a probability of 1/4 due to its route options on the colored lattice.\\n- For ID [1], there was an incorrect calculation of card arrangements, indicating a need to refine the permutation strategy accounting for overcounts.\\n- For ID [3], there was a misunderstanding regarding the calculation of dimensional fitting resulting in an incorrect probability. Multiple configurations need to consider valid shared sets.\\n- For ID [4], the probability was miscalculated because successful selections were incorrectly noted.\\n\\n3. Changes to `str0` and `str1` aren't necessary since they provide the context or style for `format` function but don't directly address the issue in the logic or computations which are the sources of errors. Instead, helping to fix reasoning or adding checks can help in evaluating problems with refined logic.\",\n", + " \"answer\": \"Based on feedback, correct calculations are:\\n- ID [0]: Probability is 1/4\\n- ID [1]: Correct total is 52 arrangements\\n- ID [3]: Correct value for sum of numerator and denominator is 5\\n- ID [4]: Correct fraction is 1/30\",\n", + " \"suggestion\": {}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3: 100%|██████████| 2/2 [00:19<00:00, 9.69s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The problem involves changing the values of variables `str0` and `str1` to improve the output based on the feedback given. The code uses the `format` function and `Learner.call_llm` function, where the outputs depend on how accurately the problem statements are understood and processed. The feedback indicates that the outputs generated by the models are not aligning with the official correct answers for the given problems, and thus need to be revised. \\n\\n1. For the first LLM call (regarding the ant problem), the answer was supposed to recognize the even-odd structure of the lattice and use that to find the probability of 1/4, but it instead produced a complex explanation with no direct conclusion. To improve this, the input should better direct the model to focus on the parity aspect of the moves. \\n\\n2. For the card arrangement problem, the model generated 72 as the number of arrangements where 5 cards remain in order after removing one card, but the correct answer is 52. The model needs refined guidance to correctly count the unique arrangements possible. \\n\\n3. The handshake problem was correctly answered, so no change is needed. \\n\\n4. For the random box problem, the computation of probability and fitting arrangements seem flawed, with the official answer stating that the probability solution should lead to a final sum of 5 instead of 3. \\n\\n5. Lastly, the probability calculation from word selection is incorrect due to misdistribution of letter selections across given word sets, needing corrections in calculating successful outcomes more precisely.\",\n", + "\"answer\": \"Based on the problem's requirements and the feedback provided, here is what can be corrected:\\n\\n1. The probability for the ant problem should factor in the parity of moves affecting the final position, focusing on how the color or parity of dot influences his net movement. \\n\\n2. Amend counting strategy for card permutations by properly accounting for unique valid sequences.\\n\\n3. Address the dimension-fitting method in the box problem by ensuring all variable or size conditions are properly resolved.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"For each modeling scenario, clarify conditions and ensure simple models can relate square position or logical outcomes clearly in solving lattice, permutation, and probability task assessments.\",\n", + " \"str1\": \"In solving these problems, highlight any unnoticed symmetry or parity aspect directly within logical reasoning, ensuring card arrangement and selection results align with intended permutations for correct model output alignment.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/8: 100%|██████████| 5/5 [00:17<00:00, 3.44s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/8: 100%|██████████| 5/5 [00:28<00:00, 5.61s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/8: 100%|██████████| 5/5 [00:23<00:00, 4.61s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/8: 100%|██████████| 5/5 [00:15<00:00, 3.14s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/8: 100%|██████████| 5/5 [00:22<00:00, 4.51s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/8: 100%|██████████| 5/5 [00:27<00:00, 5.59s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/8: 100%|██████████| 5/5 [00:24<00:00, 4.89s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/8: 100%|██████████| 5/5 [00:33<00:00, 6.60s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.0000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 2 - Best validation score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 3/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 3\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:19<00:00, 3.81s/it]\n", + "Generating 2 proposals for beam 1: 50%|█████ | 1/2 [00:10<00:10, 10.73s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The feedback highlights that each response fails to provide a specific numerical answer to the posed problems. The 'batchify24' output is essentially a collection of general strategies for solving mathematical problems rather than specific solutions to each prompt. Each Learner.call_llm invocation intends to solve a specific mathematical problem described by the corresponding 'message', but the current approach does not align these strategies with specific computational elements or mathematical formulas related to each problem. The key is to focus on numerical solutions using specific data given in each problem statement. The 'format' strings, composed from 'str1' and 'message', do not inject the required problem-solving logic since 'str1' is a generic problem-solving strategy. A change is needed for 'str1' to directly prompt specific computations for each mathematical problem described by 'message345' to 'message349'.\",\n", + " \"answer\": \"The batchify output should contain specific, computed numerical results for each individual mathematical problem that messages 345 to 349 describe.\",\n", + " \"suggestion\": {\n", + " \"str1\": \"Calculate the specific numerical solution for each problem: 1) message347: calculate number of valid card arrangements, 2) message349: calculate ways to make substitutions and find remainder, 3) message346: calculate distinct collections of letters, 4) message345: calculate number of fish on May 1, 5) message348: determine the last locker number opened.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1: 100%|██████████| 2/2 [00:11<00:00, 5.99s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks for modifying the value of the variables in #Variables to improve the output according to the feedback provided. The feedback indicates that the outputs contain general strategies for problem-solving rather than specific numerical answers to the given mathematical problems. The variables str0 and str1 are used for creating formatted prompts for LLM models, which are then used to solve specific math problems described in message345 to message349. The current values of str0 and str1 imply a need for a logical structure and verification, but they do not directly guide the model to solve specific problems or reach numerical answers.\\n\\nTo address the feedback, we need to set str0 and str1 to suggest focusing on solving each specific math problem, performing required calculations, and determining numerical answers:\\n\\n- For str0, since it prompts the system with a goal, it should be aimed at solving mathematical problems and providing numerical answers.\\n- For str1, as it's used to format direct prompts for solving problems, it should encourage specific problem-solving and computation.\\n\\nThese changes should guide the model towards calculating and outputting specific numerical answers for each problem.\",\n", + " \"answer\": \"The given feedback indicates errors due to missing numerical results for each math problem. The changes needed should focus on directing the model to calculate specific results for each problem using proper computations.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Direct the model to solve the specific math problems provided, calculate the required results, and output precise numerical answers for verification.\",\n", + " \"str1\": \"Focus on computing the specific solutions to given problems using mathematical and logical processing, ensuring that the final result is a clear numerical answer.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:31<00:00, 6.23s/it]\n", + "Generating 2 proposals for beam 2: 50%|█████ | 1/2 [00:08<00:08, 8.62s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction asks to change the values in #Variables to improve the output in accordance to #Feedback. This involves analyzing why the current variables lead to incorrect answers and adjusting them. 2. The feedback indicates that the student's answers in the batchify26 output do not match the expected outputs for the specific mathematical problems mentioned in the inputs associated with the call_llm functions. The variables str0 and str1 set the context for the logical and systematic solving of the problems, but they appear to not directly address the individual computation requirements of the math problems stated in the Inputs section. 3. Suggestions for changes need to focus on aligning str0 and str1 more closely with the exact requirements of the individual mathematical problems. This includes specifying more directly how to use combinatorial and symmetrical logic specific to arranging cards, handling substitutions, calculating fish population, etc., based on the description of the specific problem constraints.\",\n", + " \"answer\": \"The current Incorrect Feedback indicates a need for a more precise rendering of str1 to deal directly with the experimental mathematical context.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Re-solve each unique problem by focusing on combinatorial logic specific to each task. Analyze patterns of assignments and orderings in arrangements.\",\n", + " \"str1\": \"Apply precise calculations to distinct mathematical problems, characterizing each by its own set of operations in context. Focus on exact policy for numeric conclusions depending on specified scenarios, adjusting indistinguishable logic.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2: 100%|██████████| 2/2 [00:11<00:00, 5.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction tells us to change the values of variables in #Variables to improve the output based on #Feedback. 2. The feedback indicates that the current output addressed the wrong problems in each section and hence the final answers do not match the expected results. For example, it mentions section outputs unrelated answers to the math problem that were intended related to card arrangements, substitutions, triangle colorings, and others. 3. Given the problem descriptions and #Documentation, it is necessary to adjust the templates in the variables str0 or str1 so that the prompts generated for the LLM correctly address the intended problems associated with the messages 350 to 354. This may involve explicitly focusing on the exact mathematical operations needed, like permutation, combination, or modular arithmetic, as these seem to be relevant based on the types of equations and results given in the Feedback.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"To solve each problem, focus on the exact numeric solutions by calculating distinct arrangements and using modular arithmetic as needed. For the card arrangement problem, determine ascending or descending sequences where one card is removable; for the locker problem, identify perfect squares; for the substitution problem, find series sums modulo 1000; for the triangles, calculate color combinations; for the fish population, solve for proportions. Ensure step-by-step alignment with the stated mathematical operations, leading to final answers consistent with expected outputs.\",\n", + " \"str1\": \"Base solutions directly on numeric calculations using appropriate combinatorial logic and modular arithmetic. For card arrangements, verify ascending and descending patterns per card removal; in lockers, rely on perfect square evaluation; in substitutes, sum series to modulo 1000; in triangles, multiply color pattern options; and in fish population, correlate tagged ratios to total estimates accurately. Carefully follow each problem's instruction for achieving final detailed numeric results.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:21<00:00, 4.29s/it]\n", + "Generating 2 proposals for beam 3: 50%|█████ | 1/2 [00:06<00:06, 6.60s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction asks to adjust the values in #Variables to improve the output, i.e., ensure the logic in the code correctly addresses the given problems. 2. The feedback indicates that the current code execution does not correctly address the distinct mathematical problems described in the inputs. The feedback suggests that the current solutions are not providing specific numerical answers aligned with official answers, and the prompts given to the models are not specific to each problem. 3. The suggestion involves adjusting the `str0` and `str1` variables to tailor the LLM calls specifically towards generating answers or calculations relevant to each problem, so each LLM call can potentially produce outputs more aligned with the expected mathematical solutions. This includes modifying the prompts to focus on solving each problem individually.\",\n", + " \"answer\": \"The current formatting and prompts are too general and do not solve the specific problems defined by each message. They do not generate targeted solutions or analyses specific to the problem instances.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Please solve the following specific problems using relevant combinatorial logic: 1. Arrange cousins in identical rooms. 2. Calculate probability in a sequence using bubble sort. 3. Calculate probability of an ant on a lattice. 4. Determine positions of switches. 5. Arrange cards in a row allowing for one removal.\",\n", + " \"str1\": \"Please solve each problem by finding specific arrangements or probabilities: 1. Cousins in identical rooms given specific constraints. 2. Sorting sequence and probabilities pertaining to bubble sort. 3. Lattice traversal probabilities. 4. Switch positions through divisor step analysis. 5. Card arrangements allowing for one removal.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3: 100%|██████████| 2/2 [00:09<00:00, 4.88s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks to change the values of the variables in #Variables to improve the output according to the #Feedback. The #Feedback indicates that the current responses do not address the specific mathematical problems in the corresponding 'message' variables (message355, message356, etc.) or provide the correct outputs. The functions call_llm with different ModelWrapper instances and user promts formatted by variables like message355 are supposed to calculate or solve these mathematical problems, but they don't return the expected results. The main issue is that the input variables str0 and str1 are not directly related to the specific questions or problems outlined in the messages. Therefore, to improve the output, str0 and str1 need to be more relevant or contextually linked to the mathematical problems described in the messages. This will enhance the prompt used by the call_llm function, potentially leading to the correct solutions. However, without explicit connection of str0 and str1 to the specific problems described, it's challenging to determine what content should be used in str0 and str1. A possible approach is to customize these prompts with problem-solving strategies, hints, or instructions more directly related to the respective mathematical problems, ensuring the prompts generated in the format function guide the LLM toward the correct answers.\",\n", + " \"answer\": \"The answer is not explicitly given, but the general solution requires customizing str0 and str1 with problem-specific content.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"To solve the mathematical problem effectively, focus specifically on the details and constraints described, applying relevant combinatorial and mathematical principles.\",\n", + " \"str1\": \"Concentrate on the problem's requirements, considering factors like symmetry, arrangements, and possible constraints to divide and conquer the task.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/9: 100%|██████████| 5/5 [00:04<00:00, 1.22it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/9: 100%|██████████| 5/5 [00:35<00:00, 7.03s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/9: 100%|██████████| 5/5 [00:18<00:00, 3.73s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/9: 100%|██████████| 5/5 [00:20<00:00, 4.03s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/9: 100%|██████████| 5/5 [00:36<00:00, 7.22s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/9: 100%|██████████| 5/5 [00:32<00:00, 6.42s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/9: 100%|██████████| 5/5 [00:29<00:00, 5.91s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/9: 100%|██████████| 5/5 [00:22<00:00, 4.47s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/9: 100%|██████████| 5/5 [00:20<00:00, 4.05s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 3 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 4/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 4\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.14s/it]\n", + "Generating 2 proposals for beam 1: 50%|█████ | 1/2 [00:13<00:13, 13.36s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction requires adjusting variable values to improve the output based on the feedback provided. The feedback indicates that the outputs from the code are currently incorrect, and each learner's process appears to answer different questions than intended. For example, the learner's response about counting indistinguishable triangles was criticized for being irrelevant and an alternative approach was suggested. The suggestion involved calculating combinations of colors for the triangles' corners and multiplying these by the number of choices for the center triangle.\\n\\nSimilarly, the learners' attempts to solve other problems, like the probability or the final locker number, didn't correctly address the key elements or calculations demanded by these questions. \\n\\nThe code constructs user prompts using 'str0' and 'str1,' which are then supposed to represent the system and user prompts for the calls to the models. It seems these prompts aren't contextualizing the problem or pointing the LLM to the specific conceptual elements needed to solve the unique problems. Therefore, the answers end up off-mark according to the feedback.\\n\\nAdjusting 'str0' and 'str1' to match the correct logic pattern required for each problem may lead to better contextual responses from the models. Specifically, aligning 'str1' towards more elaborative, problem-specific conditions might help the LLM generate correct solutions.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Each problem needs a distinct solution: calculate distinguishable triangles based on color configurations for their corners and center triangle, compute Locker 342's toggling sequence, refine the probability structure for r_{20}'s position swap, and reconsider the probability of ant's path after 5 moves.\",\n", + " \"str1\": \"Directly apply combinatorial logic to each problem: Utilize distinct problem-tailored prompts to achieve precise and contextualized LLM outputs addressing scenarios such as triangle configurations, locker toggling behavior, order probabilities, and ending positions.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1: 100%|██████████| 2/2 [00:14<00:00, 7.01s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction requires adjusting variable values to improve output, which means aligning them with correct understanding and context of the problem as per feedback. 2. The feedback highlights that the current outputs do not match the official correct answers, indicating that the logical approach or interpretation was incorrect for each problem instance. Diagnosis of each problem's logical resolution needs rectification to conform to intended problem constraints. 3. Suggestions will focus on aiding the numerical alignment of context-induced adjustments per model's requirements. - For str0 and str1, the variable values are elaborate problem descriptions that seem to not directly connect to model responses expected, indicating excessive skeleton logic. Adjustments must hone on the precise factoring detail required for models concerning specific construct scenarios rather than excess narrative regurgitation. Each Learner.call_llm.response mismatches feedback clarity, with adjustments needed to submit appropriately line-precise query inputs aligning with decimalized outputs required (e.g., permutation representations or iterative derivatives). These set conjectures require direct inputs reducing excess extraneous reasoning. Expectations adjust per slight error logic on grounding problem factors (i.e., refined multiset/permutation blend logic must be regularized without straying interpretations). Therefore, providing direct numeric values/solutions should follow minimal context: 4 or 6 steps, highest locker open, and so forth pediatric constrained placement aligned to stir context aptly—check correct numeric correlation displays per evaluated category tallying (without adding digressions). Batched solutions adopting main prompt expectations conciliates direct specificity and will be necessary monitoring yield check for str1,\", \n", + "\"suggestion\": {\n", + " \"str0\": \"Focus meticulously on numerical results aligning to prompt specifics inquired, ensuring model proper tally via binomial or multiset logic specificities without diverged temporary discrepancy elaborations.\",\n", + " \"str1\": \"Apply logic that strictly tags permutations, specific models, inductive color pairings in patterns maximizing accuracy across finalized locker statuses and query-specific results.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:06<00:00, 1.34s/it]\n", + "Generating 2 proposals for beam 2: 50%|█████ | 1/2 [00:06<00:06, 6.47s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction directs to adjust the variable values to improve code output based on feedback. 2. Feedback indicates that the current output is incorrect because the students' responses ask for problems rather than calculating and providing numerical answers to the questions posed in the inputs. The code generated by `format` isn't working as expected to communicate instructions for solving the problems. The current input variables, `str0` and `str1`, are meant to direct Systems using the `Learner.call_llm` to handle math problem solutions, but they must be more directed to achieve the intended outcome of calculating answers rather than prompting for problems. 3. Suggested changes: Adjust `str0` and `str1` to explicitly request that the system calculate the answers to the provided prompts. Expected results: Explicit instructions should lead to a numerical answer output. The completion would then address the math problems directly, providing solutions rather than asking for more problems.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"Calculate the final numerical answers for the provided math problems and ensure to output them accurately.\",\n", + " \"str1\": \"Execute the calculations using mathematical and logical processing, ensuring the output is the final numerical answer for each provided problem.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2: 100%|██████████| 2/2 [00:07<00:00, 3.74s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The #Instruction stipulates modifying the values in #Variables to improve the outputs based on the #Feedback provided.\\n\\n2. The #Feedback consistently points out that the responses (outputs) do not contain the required specific numerical answers or results matching the official correct answers, which are needed for proper evaluation.\\n\\n3. The current #Code uses system and user prompts (str0 and str1 replaced values) to generate outputs with Learner.call_llm, but these outputs lack the actual computations needed for each mathematical problem described in #Inputs.\\n\\n4. Both str0 and str1 are descriptions for mathematical problem-solving, and str1 already emphasizes providing a numerical answer. To align directly with the feedback, str0 and str1 need to reflect actions ensuring that the actual computations are returned, not just a solicitation for math problems.\\n\\n5. Suggesting changes involves ensuring that one of these variables reflects the importance of providing the final numerical answer required in response, such as by including a succinct directive for generating specific solution outputs based on the problem mentioned in the variable messages.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Upon receiving a math problem, compute the specific and precise numerical result based on problem instructions and assumptions given.\",\n", + " \"str1\": \"For each given math problem, determine the comprehensive steps to arrive at a precise numerical final answer and present it clearly.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (batch size: 5): 100%|██████████| 5/5 [00:23<00:00, 4.68s/it]\n", + "Generating 2 proposals for beam 3: 50%|█████ | 1/2 [00:20<00:20, 20.76s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The #Instruction requires improving the output based on #Feedback. The #Feedback for each problem outlines specific misinterpretations or missing calculations. Thus, the variables str0 and str1 need adjustments to lead to correct problem solving and outputs.\\n\\nIn general, the provided prompts in str1 don't align specifically with the detailed calculations needed for each question, leading to unnecessary generic outputs and incomplete computation. Adjustments should ensure that the prompts involve calculations and constraints specific to the inputs provided.\\n\\nFor example, str1 should be adjusted to directly instruct calculations for messages 415 to 419 based on known mathematical solutions (like combinations, permutations, probability, etc.).\\n\\nI will adjust str1 to reflect specific problem-solving instructions related to each message number (message415 to message419) as noted in #Feedback for the correct calculation method and instructive detail for each separate problem.\",\n", + " \"answer\": \"To improve the calculations, adjust str1 to precisely target the expected mathematical solutions as directed by #Feedback.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Rely on specific mathematical reasoning for each problem, ensuring the system prompt aligns with expected calculations and final conclusions.\",\n", + " \"str1\": \"Calculate the mathematical solutions for each problem: 1) Calculate the probability of selecting the letters forming 'PROBLEM' from given sets, 2) Determine the combinations of letters in constrained sets like 'MATHEMATICS', considering indistinguishability, 3) Compute the probability of the ant's specific moves over a lattice, 4) Apply modular arithmetic to understand the sequence of locker actions, and 5) Solve based on handshake problems in combinatorics for team arrangements.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3: 100%|██████████| 2/2 [00:22<00:00, 11.44s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The #Instruction asks for changing the values in #Variables to improve the output according to #Feedback.\\n2. The #Feedback gives specifics about what is expected for each problem presented in the #Outputs. For instance, in ID [0], the correct approach is calculating the probabilities for Joe's selections from words CAMP, HERBS, and GLOW. Similarly, in ID [1], it's about calculating the number of distinct letter collections in MATHEMATICS. The feedback clarifies the expected outcomes and provides official answers, like a probability of 1/30 or a total of 72 distinct letter collections.\\n3. Based on the #Feedback, each problem in the #Output needs a tailored approach:\\n - For ID [0], we can improve by ensuring to compute the probability of forming the word PROBLEM based on specific selections from CAMP, HERBS, and GLOW. Given message415, this requires calculating the probability of selecting the requisite letters from each word, with the expected probability being 1/30.\\n - For ID [3], the expected answer is that the last locker opened is 342, not 961. This involves understanding the pattern of the student's locker problem and correcting the strategy for toggling lockers.\\nTherefore, setting 'str0' and 'str1' more explicitly towards achieving these calculations is likely the focus.\", \n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"Please calculate the probability that Joe selects 'P', 'R', 'O', 'B', 'L', 'E', 'M' from the given letters in CAMP, HERBS, and GLOW in that specific order. This should result as a common fraction denoting the probability, ensuring it results in 1/30.\",\n", + " \"str1\": \"Calculate and ensure distinct mathematical solutions for: 1) number of valid card arrangements, 2) calculating replacements and remainders, 3) distinct letter collections focusing on MATHEMATICS letters falling off, 4) number of fish change analysis instead of last locker, and 5) evaluate last locker opened as locker 342.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/9: 100%|██████████| 5/5 [00:16<00:00, 3.39s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/9: 100%|██████████| 5/5 [00:35<00:00, 7.04s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/9: 100%|██████████| 5/5 [00:32<00:00, 6.55s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/9: 100%|██████████| 5/5 [00:14<00:00, 2.92s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/9: 100%|██████████| 5/5 [00:08<00:00, 1.73s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/9: 100%|██████████| 5/5 [00:06<00:00, 1.34s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/9: 100%|██████████| 5/5 [00:17<00:00, 3.40s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/9: 100%|██████████| 5/5 [00:24<00:00, 4.81s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/9: 100%|██████████| 5/5 [00:33<00:00, 6.72s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 4 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[96m\n", + "Best parameters at depth 4:\u001b[0m\n", + "\u001b[96mstr:0: Solve each problem by directly calculating distinct answers. For the object reaching (2,2) in steps, determine all ways in 4 or 6 steps. For the lockers, trace each open-close step carefully until locker 342 is the last. For gymnasts, calculate combinations where total is precisely 281, minimizing coach handshakes. For cousins, enumerate placement variants for each room combination. For letters, determine indistinguishable combinations focusing on T's, M's, and A's.\u001b[0m\n", + "\u001b[96mstr:1: Focus directly on providing numeric answers by applying combinatorics, symmetry, and dimensions fitting logic specific to the distinct problem being queried, ensuring detailed, step-by-step solutions addressing the exact scenarios described.\u001b[0m\n", + "\u001b[96m\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating best parameters at depth 4 on test set: 100%|██████████| 10/10 [01:00<00:00, 6.03s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[95mDepth 4 - Test score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Final Selection Using Full Validation Set =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/3: 100%|██████████| 20/20 [01:48<00:00, 5.45s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0500\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/3: 100%|██████████| 20/20 [01:09<00:00, 3.46s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/3: 100%|██████████| 20/20 [02:31<00:00, 7.58s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0500\u001b[0m\n", + "\u001b[92mSelected top 1 beams with scores: ['0.0500']\u001b[0m\n", + "\u001b[95m\n", + "===== Final Proposal Candidate Parameters =====\u001b[0m\n", + "\u001b[94mstr:0: Solve each problem by directly calculating distinct answers. For the object reaching (2,2) in steps, determine all ways in 4 or 6 steps. For the lockers, trace each open-close step carefully until locker 342 is the last. For gymnasts, calculate combinations where total is precisely 281, minimizing coach handshakes. For cousins, enumerate placement variants for each room combination. For letters, determine indistinguishable combinations focusing on T's, M's, and A's.\u001b[0m\n", + "\u001b[94mstr:1: Focus directly on providing numeric answers by applying combinatorics, symmetry, and dimensions fitting logic specific to the distinct problem being queried, ensuring detailed, step-by-step solutions addressing the exact scenarios described.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating best beam on test set: 100%|██████████| 10/10 [00:54<00:00, 5.48s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92mBEST BEAM - Test score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Periodic Test Scores Summary =====\u001b[0m\n", + "\u001b[96mDepth 1: Test score = 0.2000\u001b[0m\n", + "\u001b[96mDepth 4: Test score = 0.0000\u001b[0m\n", + "FINISHED TRAINING BEAM SEARCH\n", + "\n", + "Best validation scores at each depth:\n", + " Depth 1: 0.6000\n", + " Depth 2: 0.0000\n", + " Depth 3: 0.2000\n", + " Depth 4: 0.2000\n", + "Final score: 0.0\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = BeamsearchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING BEAM SEARCH\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING BEAM SEARCH\")\n", + "\n", + " if 'best_validation_scores' in metrics:\n", + " print(\"\\nBest validation scores at each depth:\")\n", + " for depth, score in enumerate(metrics['best_validation_scores']):\n", + " print(f\" Depth {depth+1}: {score:.4f}\")\n", + " \n", + " print(\"Final score: \", final_score)\n", + " \n", + "asyncio.run(wrapper())" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING BEAM SEARCH w/ HISTORY\n", + "\u001b[94mRunning BeamsearchHistoryAlgorithm with beam_width=3, max_depth=4, max_history_size=2\u001b[0m\n", + "\u001b[94mUsing validation_dataset_size=5 for intermediate evaluations\u001b[0m\n", + "\u001b[94m\n", + "===== Evaluating Initial Parameters =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating initial parameters on test set: 100%|██████████| 10/10 [00:59<00:00, 5.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[93mInitial test score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 1/4 with 1 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 1\u001b[0m\n", + "\u001b[93mProcessing beam 1/1\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 1, batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.03s/it]\n", + "Generating 2 proposals for beam 1 (with history): 100%|██████████| 2/2 [00:18<00:00, 9.20s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction requires modifying the values of the variables in #Variables to improve the output. 2. Based on the feedback, it is evident that the calculations or expected outputs do not match the Official Correct Answer. Many of the provided answers do not align with the expected numbers such as m+n = 67, which appears to relate to reaching (2,2) in six or fewer steps in a given problem (assuming the task requests this directly by formula derivation), probability of being at dot B is 1/4 for the ant problem, 72 for the collection of letters problem, 560 for the sequence problem, and 336 for the distinguishable triangles. All these need revisiting. 3. The feedback indicates each computation feature problems deviating from core constraints needed or crossing excess assumptions weakening finite numerical submission. 4. Suggestion: I suggest refining str0 and str1 further to focus on precise calculation statements addressing each scenario uniquely but correctly. Using simplified probability formulation or exact factorial derivation honing mismatches until respective outcomes align, then reconfirm calculations/problems recognizing chance at dot B resultant impact after 5 moves, distinct subsequence occurrences probability in trials within locks or combined permutations of space arrangements aligning single logic procedural correlation establish comprehensive boundaries.\",\n", + "\"answer\": \"\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Start by addressing the key combinatorial or probability problems described. For instance, consider only operations, calculations needed, exact position probability for objects without further redundancy - i.e. distinct steps, adjacent counting with implied locking sequences accurately deriving odds satisfied, arrange magnets in known conditions for distinct number collections using factorial methods ensuring results matching output findings.\",\n", + " \"str1\": \"Review precisely derived results, analyzing combinatorial/geometry fitting descriptions providing incremental measures incrementing geometries by direct summation, probability calculations summing rational scenarios noted for different problems including equals achieving desired arrangement interacting calculated sequences, verifying final auxiliary operation reversals.\"\n", + "}\n", + "}\n", + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. #Instruction asks for changes in #Variables to improve the output based on #Feedback, which indicates that the current output is incorrect and the main task is to identify which values influence the wrong output and adjust them accordingly.\\n\\n2. #Feedback suggests that the solutions provided in the output did not match the expected results based on specific combinatorial problems. Specifically, for the problem related to reaching point (2,2), a specific numerical solution was expected but not provided. The feedback mentions an expectation for calculating probabilities and numerical results that align with official answers such as m+n=67 for certain scenarios. Similarly, for other problems, specific answers were expected, like certain probabilities or counts, which were not delivered by the current reasoning in the output.\\n\\n3. The #Variables 'str0' and 'str1' provide contextual explanations or prompts used in the code. The feedback mentions aiming for probabilities and precise combinatorial solutions, suggesting that these descriptions may not emphasize the correct method or thought process needed to guide the model (Learner.call_llm) correctly toward the specified outputs.\\n\\n4. By understanding what the official solutions require and how the current format strings ('str0' and 'str1') might set up the problem incorrectly, we can revise these prompts to better focus on the correct combinatorial or probability analyses and thus achieve the expected results.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"For each mathematical scenario, calculate the precise probability or combinatorial result by analyzing the given conditions. Ensure all outcomes match expected numerical results such as m+n=67, exact handshake probabilities, and specific distinguishable counts based on provided parameters.\",\n", + " \"str1\": \"Use mathematical rigor to solve problems by focusing on combining correct probability distributions, exact permutations, and alignment with official results for each described scenario, incorporating precise steps for calculation adherence.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/3: 100%|██████████| 5/5 [00:19<00:00, 3.81s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/3: 100%|██████████| 5/5 [00:22<00:00, 4.50s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/3: 100%|██████████| 5/5 [00:30<00:00, 6.14s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mKeeping all 3 candidates as num_candidates <= beam_width. Scores: ['0.0000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 1 - Best validation score: 0.0000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 2/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 2\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 1, batch size: 5): 100%|██████████| 5/5 [00:21<00:00, 4.35s/it]\n", + "Generating 2 proposals for beam 1 (with history): 50%|█████ | 1/2 [00:05<00:05, 5.57s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The instruction asks for adjusting variable values to improve the output, which is incorrect based on the feedback provided. 2. The feedback specifies that the outputs did not match the expected results due to focusing on unrelated scenarios, meaning the logic or context applied to solve the problems was incorrect for the specific problems presented. 3. The suggestion is to refine the variable values based on each specific problem, mainly focusing on correctly understanding and applying mathematical principles pertinent to each problem outlined in the messages. This implies ensuring solution context aligns exactly with each problem being solved (e.g., ant movement probabilities, switch positions, soccer substitutions).\",\n", + "\"answer\": \"n/a\",\n", + "\"suggestion\": {\n", + " \"str0\": \"A detailed mathematical analysis is needed for each problem to apply correct combinatorial principles. For example, interpret switch advancement in terms of greatest common divisor counts, enumerate valid step sequences for the ant movement problem to fractionally compute a specific path probability, and compute exact configurations or permutations for substitution problems.\",\n", + " \"str1\": \"Refocus specifically on solving defined mathematical scenarios using precise concepts like calculating remainder for permutations, probability distribution examination for movement tasks, and opening sequences to define locker task outcomes congruent with given scenarios.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1 (with history): 100%|██████████| 2/2 [00:16<00:00, 8.43s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The #Instruction asks to modify the values in #Variables section to improve the output in accordance with the #Feedback. \\n\\n2. The #Feedback section provides information for each problem, indicating that the student's answers were incorrect and suggests how the solutions could be approached correctly. Each problem feedback has been centered on the fact that the student's final answers did not address the specific problem at hand or the numerical solution required. \\n\\nIn particular for this execution:\\n - ID [0]: This involves the problem with ant's movements and the dots, the focus should be on the fact there are four blue dots, using which we determine the probability of ending on any specific dot (in this case, labeled $B$) as 1/4. \\n - ID [1]: This feedback is about the problem involving the switch positions. The correct approach is to use the divisor counting rule.\\n - ID [2]: This involves calculating the number of substitution methods and requires the student to establish a recursive relationship through combinatorial reasoning for possible substitutions. \\n - ID [3]: The feedback involves a probability problem where you need to verify calculations for probability steps, ensuring you get a simplified fraction and consequently sum its numerator and denominator to a value of 65.\\n - ID [4]: This explains a problem where locker sequences are tracked using pattern rules, where the last locker opened is expected to be 342.\\n\\n3. The #Constraints section is empty, suggesting no explicit constraints were given and decisions should be based on problem context. #Code shows calls to create string prompts for various models and how the results are concatenated into batchify38.\\n\\n4. Updated #Variables suggestions:\\n - str0: Change to focus directly on probability problems and combinatorial scenarios with relevant endpoints to guide LLM model towards specific results requested in Feedback.\\n - str1: Directly address calculation precision needed during combinatorial, permutation, and probability problem-solving, ensuring solutions match outcomes outlined in Feedback.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"For each specified probability or combinatorial task, compute the exact results by thoroughly analyzing provided scenarios and numerical outcomes, ensuring alignment with expected problem conditions such as precise path counts, probability distributions, and permutations.\",\n", + " \"str1\": \"Apply rigorous mathematical reasoning to each problem scenario, focusing on precise probability computation, specific combinatorial arrangements, and accurate problem-solving techniques for distinct outcomes, optimizing solutions for clarity and correctness.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 2, batch size: 5): 100%|██████████| 5/5 [00:27<00:00, 5.40s/it]\n", + "Generating 2 proposals for beam 2 (with history): 50%|█████ | 1/2 [00:11<00:11, 11.16s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instructions indicate we need to change the values of the variables to adhere to the feedback. The feedback suggests that numerical answers are missing from the outputs, so for each prompt, a numeric finish or conclusion was expected but is absent. Reflecting on this, it seems that the problem lies in the content of 'str0' and 'str1'. Each format uses 'str1' combined with a specific 'message', but the functions called in the code emphasize presenting structured approaches instead of delivering conclusions or final numerical values. Therefore, the current strings don't guide the LLM models towards computing final answers. To remedy this, 'str0' and 'str1' should be reformulated to encourage a focus on final numeric result calculations corresponding to each problem's queries. By aligning the variables with content that drives precise numerical calculations, the desired outcome should be achieved. Expected outputs need to be precise results in numerical form corresponding to combinatorial, probability, or arrangement problems.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"Provide a precise numerical answer directly related to each problem posed. Evaluate operations, calculations, and apply probability principles or combinatorial strategies as needed to derive a concrete numeric result that satisfies the problem's constraints.\",\n", + " \"str1\": \"For each problem, derive a specific numeric conclusion, leveraging combinatorial, geometric, or probabilistic calculation methods as required. The goal is a definitive answer reflecting a thorough understanding of and correct application to the distinct problem context.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2 (with history): 100%|██████████| 2/2 [00:12<00:00, 6.03s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks to improve the output by changing the values of the variables in #Variables. The feedback indicates that the outputs are incorrect because they do not provide final numerical answers to specific probability or counting problems described in the inputs. The current descriptions in str0 and str1 are too general and do not guide the models on how to generate the correct answers for the problems posed. To improve the output, str0 and str1 need to directly address the specific problems: \\n\\n1. For message580, compute the probability that two randomly interviewed students can provide information about both French and Spanish classes, considering set intersections.\\n2. For message581, calculate the largest number of elements in a subset S given constraints on differences between members.\\n3. For message582, determine the fewest handshakes a coach could have participated in given a total number of handshakes.\\n4. For message583, count how many switches remain in position A after a divisibility-based process on labeled switches.\\n5. For message584, calculate the number of ways substitutions can be made during a soccer game including specific constraints.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"Precisely solve each problem. For instance, calculate the probability of selecting a student in both French and Spanish for message580, determine the maximum subset size for message581, calculate the minimum coach handshakes for message582, count remaining switches in position A for message583, and find substitution permutations mod 1000 for message584.\",\n", + " \"str1\": \"Use detailed combinatorial and probability calculations to directly determine numerical answers matching each problem's constraints, verified through reevaluation of logical steps.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 3, batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.20s/it]\n", + "Generating 2 proposals for beam 3 (with history): 50%|█████ | 1/2 [00:09<00:09, 9.97s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks us to modify the values of variables to improve the output based on feedback. Each feedback corresponds to a distinct problem, and each problem corresponds to a specific message input in the code. For the given problems, we need to rethink the provided solutions and align them with the true answer as discussed in the feedback:\\n\\n1. For the problem about 'MATHEMATICS' and word arrangement, instead of the large number initially suggested, it should be verified that the answer is 72 by recalculating the ways to select and arrange the vowels and consonants as given.\\n\\n2. For the biologist fish problem, by recalculating the population estimates using proportions as explained in the feedback, adjusting for tagged and untagged fish under given conditions, the true initial population should be determined as 840.\\n\\n3. For the set problem involving no two numbers differing by 4 or 7, as explained, the count through proper combinatorial selection within constraints should yield a result of 905.\\n\\n4. For the problem about cousins and room arrangements, focusing only on valid combinations without incorrect assumptions should yield an official result of 15.\\n\\n5. Lastly, for the soccer substitution problem, careful calculation of possible substitution cases and summing them should yield an answer of 122.\\n\\nIn conclusion, we will implement adjustments in the string prompts for each relevant problem to reflect this accurate reasoning and obtain the correct outputs.\",\n", + " \"answer\": \"The adjustments are needed for each problem string associated with the described variables.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Use correct counting methods to verify or derive small, well-defined numerical results matching each problem's official answer.\",\n", + " \"str1\": \"Directly employ combinatorial and probability techniques to resolve and confirm problem constraints to reach established solutions like 72, 840, 905, 15, and 122.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3 (with history): 100%|██████████| 2/2 [00:14<00:00, 7.46s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction requires adjusting the values in #Variables to improve the outputs in accordance with #Feedback. Analyzing the feedback, it becomes clear that certain calculations or solutions did not match official results or were addressed incorrectly. Specifically, problems were identified in the following areas:\\n\\n1. Problem related to 'MATHEMATICS': The feedback points out that the calculated distinct combinations were incorrect and highlights that the correct count should be 72.\\n2. Problem related to the fish in the lake: The correct calculated number of fish should have been 840, using the given percentages and proportional reasoning.\\n3. Problem related to subset S: The calculated number of elements mistakenly did not address the correct constraints leading to an incorrect solution.\\n4. Problem related to cousins and hotel rooms: Although no final answer was provided, the expected correct arrangement combinations lead to an answer of 15.\\n5. Problem related to soccer team substitutions: The expected correct answer was 122, following specific combinatorial approaches.\\n\\nThe responses did not correctly apply combinatorial logic or provide final answers for some scenarios. Errors likely arise from how specific descriptions in the prompts (#Variables) direct problem-solving processes.\\n\\nTo rectify issues, new, more accurate problem descriptions should direct responses to valid numeric conclusions. This involves more detailed, direct problem solving with concise numeric emphasis contextualized by placeholder adjustments to prompt accurate systemic logic.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"Start by addressing the key combinatorial or probability problems described. For instance, consider only operations, calculations needed, exact position probability for objects without further redundancy - i.e. distinct steps, adjacent counting with implied locking sequences accurately deriving odds satisfied, arrange magnets in known conditions for distinct number collections using factorial methods ensuring results matching output findings.\",\n", + " \"str1\": \"Revise each problem statement ensuring the execution of appropriate combinatorial or geometric steps correctly matching logical deductions. Ensure today's responses clearly translate solutions from mathematical analyses accurately and concisely across varied scenarios and verification of accurate configuration output as required by explained conditions, ensuring distinctness in positions or questions resolved by results improved.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/9: 100%|██████████| 5/5 [10:40<00:00, 128.16s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/9: 100%|██████████| 5/5 [00:29<00:00, 5.89s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/9: 100%|██████████| 5/5 [00:40<00:00, 8.12s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/9: 100%|██████████| 5/5 [00:19<00:00, 3.86s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/9: 100%|██████████| 5/5 [00:40<00:00, 8.15s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/9: 100%|██████████| 5/5 [00:27<00:00, 5.45s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/9: 100%|██████████| 5/5 [00:29<00:00, 5.87s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/9: 100%|██████████| 5/5 [00:29<00:00, 5.99s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/9: 100%|██████████| 5/5 [00:29<00:00, 5.90s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 2 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 3/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 3\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 1, batch size: 5): 100%|██████████| 5/5 [00:30<00:00, 6.10s/it]\n", + "Generating 2 proposals for beam 1 (with history): 50%|█████ | 1/2 [00:12<00:12, 12.14s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction asks to change the variable values in #Variables to improve the output according to #Feedback. The feedback indicates that the issues arise from the provided answers not being specific to the problems each message635-message639 describe. Each message describes a distinct probability or combinatorial problem, yet the outputs are filled with general examples unrelated to these problems. The str0 and str1 variables are inputs for these messages, and they are too generic and don't guide the system's response towards the specific problem scenarios given in the messages. Changing these to specific guidelines related to each specific problem could direct the model towards more problem-specific outputs.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"Calculate precise combinatorial or probability results specifically for the given scenarios.\",\n", + " \"str1\": \"For each specific scenario described, utilize exact mathematical techniques to produce the precise probability or count of outcomes, matching the problem's requirements given in detail.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1 (with history): 100%|██████████| 2/2 [00:17<00:00, 8.85s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The #Instruction asks to adjust the values of the variables in #Variables to improve the output based on #Feedback. 2. The #Feedback indicates that the current outputs do not answer the specific mathematical problems prompted by each unique message. Instead, the examples provided do not correspond to the problems stated by the messages in #Inputs. 3. The variables str0 and str1 in #Variables are system and user prompts that precede the presentation of each mathematical problem and do not actively engage in solving the specific problem statements. The default prompts are broad and do not relate directly to the details in each message input, leading to unrelated or incorrect outputs. 4. For example, in message635, the problem is about arranging four cousins in four identical rooms, requiring a combinatorial solution specific to that context (there are 15 ways distinct different ways), but the examples provided are about unrelated generic probability and combinatorics examples. 5. To improve alignment, str1 should be tailored to directly address the specific problem scenarios from each message input. However, str0 is detailed in complexity beyond the need for basic problem alignment. The suggestion is to adjust the str1 variable to include indications to address the specific problem scenarios presented in the messages, while str0 guides the overall problem-solving strategy.\",\n", + " \"answer\": \"N/A\",\n", + " \"suggestion\": {\n", + " \"str1\": \"Solve the specific problem: {message} using appropriate mathematical principles and provide the correct result ensuring adherence to the problem requirements and specifics.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 2, batch size: 5): 100%|██████████| 5/5 [00:31<00:00, 6.34s/it]\n", + "Generating 2 proposals for beam 2 (with history): 50%|█████ | 1/2 [00:08<00:08, 8.63s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The problem requires altering the variables to improve the output, which in this context relates to maximizing the effectiveness of the string prompts used in constructing format strings for calling LLM models. The feedback indicates that the outputs produced from call_llm methods are not providing numerical solutions or conclusions required by each specific problem they address. This suggests that the variable 'str0' or 'str1' used in the format method should specifically address the prompt requirements for each mathematical problem rather than using a generic mathematical reasoning prompt. Each message variable ('message640' to 'message644') corresponds to a different problem, as described in the feedback sections. Thus, a customized approach in formulating 'str1' could improve the relevance and accuracy of the outputs from the LLM model by ensuring the prompt directly relates to the problem specifics in 'message' variables.\",\n", + " \"answer\": \"Invalid responses across multiple outputs indicate the need for more targeted prompt formation to reflect the specific requirements of the individual math problems.\",\n", + " \"suggestion\": {\n", + " \"str1\": \"Focus on deriving concrete numerical solutions for individual mathematical problems—such as coin sequence possibilities, room distribution combinations, tagged fish population calculations, set element selections, and letter typing orders—by applying problem-specific combinatorial and probability principles.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2 (with history): 100%|██████████| 2/2 [00:18<00:00, 9.06s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction asks us to modify the variables in #Variables to obtain a better output according to the #Feedback. The feedback indicates that all scenarios have been evaluated incorrectly. It is important to address each problem correctly with the appropriate method or calculation. \\n\\n- For batchify46 ID [0], the task involves counting sequences of coin tosses with specific subsequences. To fix the issue, we should follow the 'balls and urns' combinatorial method to determine valid arrangements for the given number of heads and tails in the sequences, ensuring to use the correct formula for combinatorial counting.\\n\\n- For batchify46 ID [1], the problem is to calculate the number of ways Matt's cousins can be accommodated in rooms, requiring a focus on arranging people in spaces. Employing a stars and bars or equivalent method yields the correct answer, accounting for overcounted options to reach a total of 15 distinct distributions.\\n\\n- For batchify46 ID [2], the student's focus should be on evaluating the lake fish problem through population dynamics analysis, outlined by utilizing sampling inference and application of proportions in environmental statistics.\\n\\n- For batchify46 ID [3], the task involves selecting a maximum subset from a set without violating constraints on differences. This problem revolves around modular arithmetic and optimizing choices within numbers, yielding the largest subset meeting the condition.\\n\\n- For batchify46 ID [4], it revolves around calculating typing order sequences, possibly by arranging elements via combinatorics, once exclusions and previous conditions are taken into factorization, especially how letter 9 affects upcoming arrangements, needed for `n` choices left.\\n\\nCorrecting variable definitions by ensuring clear alignment of prompts with task specificity will improve the results.\\n\",\n", + "\"answer\": \"\",\n", + "\"suggestion\": {\n", + "\"str0\": \"Compute combinatorial solutions distinctly for each problem scenario by ensuring mathematical principles remain pertinent to the problem context, such as subsequences in coin toss sequences, combinatorial configurations for population sampling requirements, arrangement complexity with spatial distribution consideration, maximum set subset considerations under constraints, and typing sequence potential after exclusionary movements.\",\n", + "\"str1\": \"Utilize accurate computational techniques to address each mathematical problem specifically, focusing on arrangement, subset calculations, population sampling, probability distributions, and how combination or permutation principles intelligently apply to single and multiple task scenarios.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 3, batch size: 5): 100%|██████████| 5/5 [00:34<00:00, 6.89s/it]\n", + "Generating 2 proposals for beam 3 (with history): 50%|█████ | 1/2 [00:11<00:11, 11.78s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction requires modifying the variable values to improve the output based on feedback, which indicates errors due to the lack of a final numerical answer for each mathematical scenario. 2. The feedback highlights missing numerical answers for each complex problem such as arranging cousins, constructing triangles, positioning switches, etc. The Code utilizes str0 and str1 as prompts for learners to call an LLM with specific scenarios. The key is ensuring that these prompts contain sufficient direction and specificity to guide users to provide numerical solutions. 3. Given these insights, the values for str0 and str1 should be enhanced to incorporate clear guidance on calculating the final numerical results required for each scenario, such as the number of combinations, probabilities, and counting outcomes. These changes should help the learners focus on the mathematical computations needed, resulting in accurate outputs.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"To precisely solve such mathematical scenarios, ensure to compute a final numerical result by thoroughly evaluating the givens. For example, when arranging indistinguishable items, count through each distribution case and sum their possibilities to achieve a definitive total. Similarly, for problems of remainder and probability, apply modulus operations effectively and cumulate probabilities. Ensure each scenario ends with a numerical result, facilitating straightforward comparisons with expected solutions.\",\n", + " \"str1\": \"Focus on resolving the mathematical scenarios with explicit numerical conclusions. Calculate the permutations or combinations required, finish with a remainder or probability outcome explicitly stated, and perform checks against anticipated solutions. In permissions or movement scenarios, follow precise combinatorial solving for counts, and ensure each scenario concludes with a directly quantified numerical outcome.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3 (with history): 100%|██████████| 2/2 [00:14<00:00, 7.38s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction asks us to modify the given variables to improve the outcome based on the feedback provided. The feedback for each problem iteration suggests that the proposed examples discuss relevant concepts but fail to reach and specify the final answers expected for the given mathematical problems. For example, the feedback frequently points out the lack of final numerical answers, which is essential for alignment with the official solution. 2. The constraints of str0 are more descriptive and less action-oriented, whereas str1 discusses the application of specific concepts to solve different mathematical scenarios. Both variables str0 and str1 need precision in addressing the problem types directly presented by the message topics. However, reviewing the feedback, str1 seems broadly aligned with the instructions but needs specificity in solutions rather than concepts alone. 3. For improving the output and correctly solving the mathematical problems, both str0 and str1 need to be directly adjusted to ensure they align with the specific requirements of each problem, focusing on detailed step-by-step solutions ending with explicit numerical results as needed in the feedback. Thus, the revisions should guide toward systematic problem-solving resulting in accurate answer derivation. \\n\\nAdditionally, the feedback and pattern recognition along the variables and intermediate results suggest common combinatorial problems with outputs explicitly defined such as possible arrangements, remainder calculations, and probability evaluations. Providing clear and accurate problem-solving pathways toward these results is paramount.\",\n", + " \"answer\": \"TERMINATE\",\n", + " \"suggestion\": {\n", + " \"str0\": \"To solve complex mathematical problems, consider direct approaches like enumerating permutations, using combinatorial evidence supported by final accurate proofs. For tasks requiring modular artithmetic, identify effective residue systems. Further, probability tasks should involve detailed distribution assessments to ensure outcomes align with calculated paths or states, finally depicting numerical results.\",\n", + " \"str1\": \"Fully formulate mathematical scenarios to achieve final accurate results per problem's nature such as remaining permutations, switch cycles, or distinguishable combinations. Utilize crisply defined sequential solutions, ensuring prompt numeric conclusions match official predictions.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/8: 100%|██████████| 5/5 [00:38<00:00, 7.63s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/8: 100%|██████████| 5/5 [00:08<00:00, 1.79s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/8: 100%|██████████| 5/5 [00:26<00:00, 5.20s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/8: 100%|██████████| 5/5 [10:42<00:00, 128.55s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/8: 100%|██████████| 5/5 [00:26<00:00, 5.25s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/8: 100%|██████████| 5/5 [00:27<00:00, 5.58s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/8: 100%|██████████| 5/5 [00:23<00:00, 4.61s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/8: 100%|██████████| 5/5 [00:38<00:00, 7.75s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 3 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Beam Search Depth 4/4 with 3 beams =====\u001b[0m\n", + "\u001b[96mSampled validation minibatch of size 5 for depth 4\u001b[0m\n", + "\u001b[93mProcessing beam 1/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 1, batch size: 5): 100%|██████████| 5/5 [00:36<00:00, 7.34s/it]\n", + "Generating 2 proposals for beam 1 (with history): 50%|█████ | 1/2 [00:15<00:15, 15.77s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The #Instruction asks us to improve the output by changing the #Variables based on the #Feedback. The feedback indicates errors in the calculations and logic used to determine the correct answers for the model's outputs. Each problem requires adjustments in the way #Variables are determined or used: \\n\\n1. For the biologist fish problem (ID[0]), a miscalculation led to an incorrect total of fish. The proportion of tagged fish needs to be reevaluated to give 840 fish. Changing the incorrect calculation by recalibrating the proportion of fish used to represent the sample tagged.\\n\\n2. For the switch problem (ID[1]), the inclusion-exclusion principle was improperly applied. It needs a more thorough analysis of the divisor multiples and count combinations to achieve the correct count of 650.\\n\\n3. For the subset problem (ID[2]), the counting of viable numbers was too high, resulting in 1421 versus the correct 905. Properly using the modulo constraints with subsets results in fewer selections than shown.\\n\\n4. The brick tower problem (ID[3]) needs reevaluation of the height variation range given constraint over block orientations. Correct arithmetic combinations should yield 465 distinct results rather than 471. Deeper combination examination between 4\\\" and 19\\\".\\n\\n5. In the box fitting probability (ID[4]), miscalculation suggests more favorable combinations than possible. Probability one-quarter follows constraints but broken for facing missing subset probabilities in the box facing group rather than prefacing 997 choices.\\n\\nAddressing these errors involves rethinking the calculations used based on model outputs, ensuring that each follows the principles correctly. Suggestions involve correcting proportions and understanding divisible elements more accurately in numbers. Expected results seen in #Feedback define the benchmark condition for accuracy.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"For each mathematical scenario, recalculate the precise probability or combinatorial result ensuring all outcomes align accurately with expected numerical solutions. Use known mathematical principles, taking care to interpret constraints and distributions effectively.\",\n", + " \"str1\": \"For each specific case, resolve the scenario using perfect mathematical reasoning with a focus on accurate probability combinations, integral formulations, and clear solution alignment to empirically grounded results as dictated by given details.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 1 (with history): 100%|██████████| 2/2 [00:19<00:00, 9.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The #Instruction asks us to adjust the variables in #Variables based on the #Feedback provided for the #Outputs. Specifically, we need to ensure that we are getting closer to the official correct answers for each output. 2. The #Feedback indicates that the outputs derived from current values do not match correct answers, suggesting that the manner in which the calculations are carried out through str0 and str1 might be inadequate or incorrect for the problems. Errors are related to the reasoning or assumptions in the mathematics, not structural code issues. 3. For str0: The description of precise probability and combinatorial results might need to be more focused or refined to match the mathematical principles and intended outcomes of each problem, as shown in the errors for individual cases. For str1: The prompt asking for solving specific problems using 'appropriate mathematical principles' might not provide enough clarity or specific direction for handling these problems effectively. Adjustments will focus both on clarifying the goal of using precise mathematical strategies and ensuring compliance with the requirements or expected results. More specifically: \\n\\n- For scenario 1 (batchify50 ID [0]), the approach might involve clearly articulating the capture-recapture method tied accurately to the problem's mathematical framework. \\n\\n- For scenario 2 (ID [1]), it might mean focusing specifically on switching cycles and ensuring error handling in module considerations. \\n\\n- Scenario 3 correction might involve more careful relation to no-difference problem setups, focusing less on excessive construction.\\n\\n- Scenario 4 might correct integer combinations of bricks for achievable configurations without overestimating. \\n\\n- Scenario 5 requires attention to probability phrasing, ensuring clear insight into conditional arrangement probabilities.\",\n", + "\"answer\": \"The values of str0 and str1 should be adjusted to better clarify the problem requirements and expectations related to mathematical principles and solutions.\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Analyze each problem scenario with clear and correct mathematical principles. Focus on precisely matching outcomes such as m+n relationships, handshake probabilities, and specific conditions provided.\",\n", + " \"str1\": \"Address each problem with exact mathematical principles. Ensure the calculated result aligns with problem specifications and official results, using methodical reasoning and problem-specific solutions.\"\n", + "}\n", + "}\n", + "\u001b[93mProcessing beam 2/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 2, batch size: 5): 100%|██████████| 5/5 [00:26<00:00, 5.22s/it]\n", + "Generating 2 proposals for beam 2 (with history): 50%|█████ | 1/2 [00:11<00:11, 11.38s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"1. The #Instruction requires improving the output by modifying the variables in #Variables based on the #Feedback. The #Feedback provides specific corrections expected in response to each problem posed in the code.\\n\\n2. Upon reviewing the #Feedback, each section indicates issues with providing correct numerical answers or relevant problem solutions, as indicated:\\n - The outputs 'Learner.call_llm695' to 'Learner.call_llm699' provide different responses to mathematical problems based on messages. However, none of them result in addressing specific provided problems, especially 'Learner.call_llm695', which should resolve to a numerical result but instead requests further scenarios.\\n\\n3. To provide a correct result for each LLM call, the specific message content related to the mathematical problems needs to be addressed correctly. The function outputs should provide not hypothetical responses but the actual numerical solutions or steps which lead to problem-solving.\\n\\n4. Suggested changes:\\n - Modify 'str0' or 'str1' to adequately stimulate providing a specific scenario or a precise answer more effectively rather than prompting further conversation. The instruction in 'str1' implies using mathematical techniques precisely, but should instead provide calculated examples based on input message specifics.\",\n", + "\"answer\": \"\",\n", + "\"suggestion\": {\n", + " \"str1\": \"In response to each provided scenario, compute exact answers using precise mathematical techniques suitable to each problem requirement and provide these directly as the output.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 2 (with history): 100%|██████████| 2/2 [00:13<00:00, 6.97s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The instruction requires modifying the values in #Variables to improve the output according to the feedback provided. 2. The feedback indicates that the outputs are incorrect because no final numerical answers are provided for the problems. Specifically, each problem (ID 0 to 4) fails to deliver a conclusive numerical solution. 3. The incorrect outputs suggest that the current combinations of str0 and str1 fail to set up the correct conditions for the ModelWrapper calls and the LLM responses. The system prompt (str0) and the user prompt context (str1) should explicitly target the intended scenario description and data. For instance, ID 0 requires clear description/setup for calculating fewest handshakes the coach could have participated in. Each str0 and str1 should be adjusted to guide the model to generate outputs aligned with the official correct answers detailed in the feedback.\",\n", + " \"answer\": \"The feedback shows that the outputs are incomplete or incorrect, so improvement is needed in the setup to elicit the correct numerical answers.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"For each solution, provide step-by-step combinatorial or probabilistic calculations resulting in precise numerical answers that match outcomes like 650 for switch positions or 931 for sequence conditions.\",\n", + " \"str1\": \"Apply mathematical accuracy for each scenario, ensuring outcomes are precise, definitive numerical results that align with the provided combinatorial or probabilistic frameworks and official answers.\"\n", + " }\n", + "}\n", + "\u001b[93mProcessing beam 3/3\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Forward pass (beam 3, batch size: 5): 100%|██████████| 5/5 [00:19<00:00, 4.00s/it]\n", + "Generating 2 proposals for beam 3 (with history): 50%|█████ | 1/2 [00:10<00:10, 10.13s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction asks us to modify the values of the variables in #Variables to improve the output based on the feedback provided. The code is attempting to format various mathematical problems and pass them to an LLM model via different format strings. Each formatted string relates to a different mathematical scenario. The feedback highlights incorrect scenarios or problems that do not directly answer the original math problems, indicating mismatches between message content and expected response. Thus, the problem description within variable 'str0' and especially 'str1' might not be precisely aligning with the specific respective mathematical problems in 'message700' to 'message704'. Each user_prompt has to be specifically prepared to perfectly fit the respective scenario described in each message. Adjust 'str1' to more precisely guide the LLM model in producing outputs specific to the problem statements in the corresponding 'message' variables.\",\n", + "\"answer\": null,\n", + "\"suggestion\": {\n", + " \"str0\": \"For each mathematical problem, analyze the scenario conditions and compute the exact probability or combinatorial result. Ensure all solutions match expected numerical results and align with problem constraints.\",\n", + " \"str1\": \"Solve each problem by focusing on using specific probability distributions, permutation calculations, or combinatorial logic tailored to the described scenarios to produce accurate outcomes that align with known results and official answers.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Generating 2 proposals for beam 3 (with history): 100%|██████████| 2/2 [00:11<00:00, 5.90s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + "\"reasoning\": \"The instruction asks to change the values of the variables in #Variables to improve the output based on the feedback. From the feedback, we understand that the current problem and attempted solutions do not match or address the correct context of the original problems they were supposed to solve. The only variables we can modify are str0 and str1, which provide the contexts/prompts for these problems. The formats and results of these contexts (str0 and str1) need to be aligned with the original problems in order to get responses that can then be properly evaluated and compared to their respective official answers. Each one of the original problems are improperly addressed as per the feedback. Therefore, to improve the output, the statements within str0 and str1 should directly refer to the specific unique mathematical problems described within the scenarios of message700, message701, message702, message703, and message704 without mixing or deviating to unrelated examples.\",\n", + "\"answer\": \"\",\n", + "\"suggestion\": {\n", + " \"str0\": \"Calculate the probability or combinatorial result for each mathematical problem given the conditions such as the secretary and letter order, the switch positions after a process, handshake counts given gymnasts and coaches, cousin room arrangements, and letter choices to form a specific word from different sets.\",\n", + " \"str1\": \"For each problem scenario, use correct mathematical techniques to solve probability or permutation issues according to the scenarios: whether it's a typing order, switch division, handshake calculation, room distribution, or letter collection to form a word.\"\n", + "}\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/9: 100%|██████████| 5/5 [00:25<00:00, 5.09s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/9: 100%|██████████| 5/5 [00:25<00:00, 5.14s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/9: 100%|██████████| 5/5 [00:32<00:00, 6.47s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.2000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 4/9: 100%|██████████| 5/5 [00:31<00:00, 6.36s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 4: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 5/9: 100%|██████████| 5/5 [00:07<00:00, 1.48s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 5: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 6/9: 100%|██████████| 5/5 [00:04<00:00, 1.06it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 6: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 7/9: 100%|██████████| 5/5 [00:31<00:00, 6.25s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 7: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 8/9: 100%|██████████| 5/5 [00:28<00:00, 5.65s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 8: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 9/9: 100%|██████████| 5/5 [00:28<00:00, 5.77s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 9: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 3 beams with scores: ['0.2000', '0.0000', '0.0000']\u001b[0m\n", + "\u001b[92mDepth 4 - Best validation score: 0.2000\u001b[0m\n", + "\u001b[94m\n", + "===== Final Selection Using Full Validation Set =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 1/3: 100%|██████████| 20/20 [03:15<00:00, 9.76s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 1: Validation score: 0.1500\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 2/3: 100%|██████████| 20/20 [01:42<00:00, 5.12s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 2: Validation score: 0.0000\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Validating candidate 3/3: 100%|██████████| 20/20 [00:45<00:00, 2.26s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mCandidate 3: Validation score: 0.0000\u001b[0m\n", + "\u001b[92mSelected top 1 beams with scores: ['0.1500']\u001b[0m\n", + "\u001b[95m\n", + "===== Final Proposal Candidate Parameters =====\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating best beam on test set: 100%|██████████| 10/10 [00:48<00:00, 4.81s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[92mBEST BEAM - Test score: 0.3000\u001b[0m\n", + "\u001b[94m\n", + "===== Periodic Test Scores Summary =====\u001b[0m\n", + "\u001b[96mDepth 1: Test score = 0.0000\u001b[0m\n", + "FINISHED TRAINING BEAM SEARCH w/ HISTORY\n", + "\n", + "Best validation scores at each depth:\n", + " Depth 1: 0.0000\n", + " Depth 2: 0.2000\n", + " Depth 3: 0.2000\n", + " Depth 4: 0.2000\n", + "Final score: 0.3\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = BeamsearchHistoryAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING BEAM SEARCH w/ HISTORY\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING BEAM SEARCH w/ HISTORY\")\n", + "\n", + " if 'best_validation_scores' in metrics:\n", + " print(\"\\nBest validation scores at each depth:\")\n", + " for depth, score in enumerate(metrics['best_validation_scores']):\n", + " print(f\" Depth {depth+1}: {score:.4f}\")\n", + " \n", + " print(\"Final score: \", final_score)\n", + " \n", + "asyncio.run(wrapper())" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "STARTING TRAINING UCB SEARCH\n", + "\u001b[96mEvaluating initial parameters using validation_dataset samples...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating candidate: 100%|██████████| 5/5 [00:32<00:00, 6.47s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[93mInitial candidate: Score 0.2000, Evals 5\u001b[0m\n", + "\u001b[94mIter 1/3: \u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Iter 1: Forward pass for action 'a' : 100%|██████████| 5/5 [00:24<00:00, 4.95s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The feedback points out errors in the calculations for each task performed by the code. The main issue across the tasks is an incorrect approach or missed key calculations that lead to incorrect results. For the sequences of coin tosses problem, the student failed to properly account for combinatorial arrangements using the 'balls and urns' model, leading to a severely inflated number of possible sequences. Similarly, for the fish population problem, the proportions were not used correctly to derive the number of fish, resulting in a projection error in the population. In the locker problem, improper tracking of the opening and closing pattern led to identifying the wrong last locker number. The card order problem had overcounting issues because of incorrectly accounting for overcounted sequences due to adjacent swaps. Lastly, the tower height estimation miscalculated possible heights due to incorrect accounting for achievable combinations. To tackle these issues, corrections involve using correct combinatorial methods, precisely tracking sequences, and correctly applying mathematical formulas or principles specified in feedback.\",\n", + " \"answer\": null,\n", + " \"suggestion\": {\n", + " \"str0\": \"This may require a custom approach aligned with the detailed feedback given for each specific problem.\",\n", + " \"str1\": \"Ensure to provide systematic breakdown and validation of the problem conditions, reacting to feedback measures described.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating candidate: 100%|██████████| 5/5 [00:32<00:00, 6.44s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mIter 1: New candidate a_prime generated. Validation Score: 0.0000, Evals: 5\u001b[0m\n", + "\u001b[95mIter 1: Added new candidate to buffer.\u001b[0m\n", + "\u001b[94mIter 2/3: \u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Iter 2: Forward pass for action 'a' : 100%|██████████| 5/5 [00:21<00:00, 4.21s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"The instruction requires improving the output based on feedback, meaning we need to modify the values in #Variables to address the issues noted in the feedback. Each feedback indicates that a numerical result or specific answer was missing in the original process, which means that the variable inputs may need alteration to ensure clear numerical conclusions. The general issue across outputs is the lack of explicit, correct numerical answers expected in problem-solving scenarios involving specific constraints and questions. The code leverages format strings to construct prompts for a language model which implies the generated output depends on these prompts’ clarity and relevance to the questions posed. These prompts could be misleading or incomplete, affecting the text output quality. Feedback suggests that the results should include specific answers derived via detailed problem-solving steps or projections using data constraints. Suggestions for changes focus on incorporating more explicit numerical or detailed answers within the text format.\",\n", + " \"answer\": \"The existing system and user prompts must be aligned to ensure the correct numerical or specific answers are provided for each problem question.\",\n", + " \"suggestion\": {\n", + " \"str0\": \"\",\n", + " \"str1\": \"Provide explicit problem-solving strategies for meeting the constraints given in the scenario. Ensure to focus on developing explicit numerical answers for each problem aspect directly related to combinatorial admissions of domino patterns and verification cycles based on user feedback or historical evaluative analysis.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating candidate: 100%|██████████| 5/5 [00:37<00:00, 7.58s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mIter 2: New candidate a_prime generated. Validation Score: 0.0000, Evals: 5\u001b[0m\n", + "\u001b[95mIter 2: Added new candidate to buffer.\u001b[0m\n", + "\u001b[92mLog @ Iter 2: Best score in buffer: 0.1000, Buffer size: 3, Total samples: 25\u001b[0m\n", + "\u001b[94mIter 3/3: \u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Iter 3: Forward pass for action 'a' : 100%|██████████| 5/5 [00:33<00:00, 6.74s/it]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM response:\n", + " {\n", + " \"reasoning\": \"1. The #Instruction requires a focus on improving the quality of outputs by changing #Variables values in accordance with #Feedback. 2. The #Feedback indicates that the outputs do not address the specific problem requirements, focusing instead on unrelated subjects like domino patterns or complex methodologies. The outputs need to connect directly with the core mathematical problems presented in #Inputs. 3. Each of the five problems presented requires distinct solutions: a probability calculation, a combinatorial typing sequence, a gymnastics handshake count, a card arrangement for sequences, and a triangle arrangement problem. Each requires a direct numerical solution. 4. Given the instruction, the value of 'str1' should help the code focus explicitly on the combinatorial problems at hand and ensure proper filling of user prompts according to input requirements. To remedy this, the prompt should directly respond to the particular problems' constraints and desired solutions.\",\n", + " \"answer\": \"Change the prompt to focus specifically on the set of five given problems to provide final numerical solutions related to probability, combinatorics of letters, handshake count, card sequences, and distinguishable triangle arrangements.\",\n", + " \"suggestion\": {\n", + " \"str1\": \"Answer the mathematical problems directly related to the given scenarios. Focus on calculating probabilities, combinatorial arrangements, or specific outcomes based on constraints provided, and present clear numerical solutions.\"\n", + " }\n", + "}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Evaluating candidate: 100%|██████████| 5/5 [00:06<00:00, 1.25s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96mIter 3: New candidate a_prime generated. Validation Score: 0.0000, Evals: 5\u001b[0m\n", + "\u001b[95mIter 3: Buffer full. Evicted a candidate (UCB: 0.5963)\u001b[0m\n", + "\u001b[95mIter 3: Added new candidate to buffer.\u001b[0m\n", + "\u001b[94mUCB search finished.\u001b[0m\n", + "\u001b[92mFinal best candidate: Mean Score 0.1000, Evals 10\u001b[0m\n", + "FINISHED TRAINING UCB SEARCH\n", + " Best candidate scores over iterations: 3 recorded\n", + " Final best candidate score: 0.1000\n", + " Final buffer average score: 0.0333\n", + "Final score: 0.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "algorithm = UCBSearchAlgorithm(\n", + " agent=agent,\n", + " optimizer=optimizer,\n", + " logger=logger,\n", + " num_threads=train_params[\"num_threads\"],\n", + " max_buffer_size=train_params[\"max_buffer_size\"],\n", + " ucb_exploration_factor=train_params[\"ucb_exploration_factor\"]\n", + " )\n", + "\n", + "async def wrapper():\n", + " print(\"STARTING TRAINING UCB SEARCH\")\n", + " metrics, final_score = algorithm.train(**train_params)\n", + " print(\"FINISHED TRAINING UCB SEARCH\")\n", + "\n", + " if 'best_candidate_scores' in metrics and metrics['best_candidate_scores']:\n", + " print(f\" Best candidate scores over iterations: {len(metrics['best_candidate_scores'])} recorded\")\n", + " print(f\" Final best candidate score: {metrics['best_candidate_scores'][-1]:.4f}\")\n", + " if 'buffer_avg_score' in metrics and metrics['buffer_avg_score']:\n", + " print(f\" Final buffer average score: {metrics['buffer_avg_score'][-1]:.4f}\")\n", + " \n", + " print(\"Final score: \", final_score)\n", + " \n", + "asyncio.run(wrapper())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "trace", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.23" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/bbh/run_prompt_bigbench_trace.py b/examples/bbh/run_prompt_bigbench_trace.py index d6b12047..23564649 100644 --- a/examples/bbh/run_prompt_bigbench_trace.py +++ b/examples/bbh/run_prompt_bigbench_trace.py @@ -1,4 +1,3 @@ -import autogen from opto.trace.nodes import node, GRAPH, ParameterNode from textwrap import dedent from opto.optimizers import OptoPrime diff --git a/examples/virtualhome.py b/examples/virtualhome.py index b4b62f21..ef392569 100644 --- a/examples/virtualhome.py +++ b/examples/virtualhome.py @@ -10,13 +10,12 @@ import opto.trace as trace from opto.trace.nodes import node +from opto.utils.llm import LLM class LLMCallable: - def __init__(self, config_list=None, max_tokens=1024, verbose=False): - if config_list is None: - config_list = autogen.config_list_from_json("OAI_CONFIG_LIST") - self.llm = autogen.OpenAIWrapper(config_list=config_list) + def __init__(self, llm=None, max_tokens=1024, verbose=False): + self.llm = llm or LLM() self.max_tokens = max_tokens self.verbose = verbose @@ -28,15 +27,15 @@ def call_llm(self, user_prompt): if self.verbose not in (False, "output"): print("Prompt\n", system_prompt + user_prompt) - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}] + messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, {"role": "user", "content": "Format your response as a JSON object."}] try: - response = self.llm.create( + response = self.llm( messages=messages, response_format={"type": "json_object"}, ) except Exception: - response = self.llm.create(messages=messages, max_tokens=self.max_tokens) + response = self.llm(messages=messages, max_tokens=self.max_tokens) response = response.choices[0].message.content if self.verbose: @@ -103,7 +102,7 @@ def fuzzy_match_action(self, returned_action, available_actions): def env_fn(env_id, env_task_set, executable_args, args): # from envs.unity_environment import UnityEnvironment - + return UnityEnvironment(num_agents=args.agent_num, max_episode_length=args.max_episode_length, port_id=env_id, @@ -186,7 +185,7 @@ def __init__(self, max_number_steps, run_id, env_fn, agent_fn, num_agents, recor self.dialogue_history_len = 30 for i in range(self.num_agents): - self.agents.append(virtualhome_agent.LLM_agent(agent_id=i + 1, args=args)) + self.agents.append(agent_fn[i]) def reset(self, task_id=None, reset_seed=1111): self.cnt_duplicate_subgoal = 0 @@ -376,7 +375,7 @@ def __init__(self, args): self.obs = None self.args = args - self.env = TraceVirtualHome(args.max_number_steps, args.run_id, + self.env = VirtualHomeEnv(args.max_number_steps, args.run_id, env_fn, args.agent_fn, args.num_agents, args=args) atexit.register(self.close) @@ -387,7 +386,7 @@ def close(self): def reset_env(self): self.env.close() - self.env = TraceVirtualHome(self.args.max_number_steps, self.args.run_id, + self.env = VirtualHomeEnv(self.args.max_number_steps, self.args.run_id, env_fn, self.args.agent_fn, self.args.num_agents, args=self.args) def reset(self, task_id=8): @@ -403,7 +402,7 @@ def reset(self, task_id=8): agent_obs, agent_obs_descs, agent_goal_specs, agent_goal_descs, agent_infos = self.env.reset( task_id=task_id) - @bundle() + @trace.bundle() def reset(agent_idx): return agent_obs_descs[agent_idx]['prompts'] @@ -446,7 +445,7 @@ def step(self, plans, agent_infos, LM_times, agent_obs, agent_goal_specs, agent_ self.obs = next_agent_obs_descs # have to add allow_external_dependencies, why metaworld is fine? - @bundle(allow_external_dependencies=True) + @trace.bundle(allow_external_dependencies=True) def step(action, agent_idx): """ Take action in the environment and return the next observation From 0ffbec3b6dd4d388f87dbc223061646bb7234639 Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 13 Aug 2025 18:03:57 -0400 Subject: [PATCH 148/172] initial commit for opro_v2 --- opto/optimizers/opro_v2.py | 185 +++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 opto/optimizers/opro_v2.py diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py new file mode 100644 index 00000000..16254834 --- /dev/null +++ b/opto/optimizers/opro_v2.py @@ -0,0 +1,185 @@ +import json +from textwrap import dedent +from dataclasses import dataclass, asdict +from typing import Dict + +from opto.optimizers.optoprime_v2 import OptoPrimeV2, OptimizerPromptSymbolSet + +""" +OPRO is a single parameter / solution optimizer that conditions on feedback. +(context, solution, feedback) -> new_solution + +It does not contain execution graph and is more streamlined/faster in inference. +""" + + +# Not inheriting from optoprime_v2 because this should have a smaller set +class OPROPromptSymbolSet(OptimizerPromptSymbolSet): + + problem_context_section_title = "# Problem Context" + variable_section_title = "# Solution" + feedback_section_title = "# Feedback" + + node_tag = "node" # nodes that are constants in the graph + variable_tag = "solution" # nodes that can be changed + value_tag = "value" # inside node, we have value tag + constraint_tag = "constraint" # inside node, we have constraint tag + + # output format + # Note: we currently don't support extracting format's like "```code```" because we assume supplied tag is name-only, i.e., + reasoning_tag = "reasoning" + improved_variable_tag = "variable" + name_tag = "name" + + expect_json = False # this will stop `enforce_json` arguments passed to LLM calls + + @property + def default_prompt_symbols(self) -> Dict[str, str]: + return { + "variables": self.variables_section_title, + "feedback": self.feedback_section_title, + "instruction": self.instruction_section_title, + } + +@dataclass +class ProblemInstance: + instruction: str + variables: str + feedback: str + + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet + + problem_template = dedent( + """ + # Problem Context + {instruction} + + # Solution + {variables} + + # Feedback + {feedback} + """ + ) + + def __repr__(self) -> str: + return self.replace_symbols(self.problem_template.format( + instruction=self.instruction, + variables=self.variables, + feedback=self.feedback, + ), self.optimizer_prompt_symbol_set.default_prompt_symbols) + + def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: + default_prompt_symbols = { + "variables": "# Variables", + "feedback": "# Feedback", + "instruction": "# Problem Context", + } + + for k, v in symbols.items(): + text = text.replace(default_prompt_symbols[k], v) + return text + +""" +TODO: +1. think about how initial solution was generated... +""" + +class OPRO2(OptoPrimeV2): + representation_prompt = dedent( + """ + You're tasked to change the proposed solution according to feedback. + + Specifically, a problem will be composed of the following parts: + - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. + - {variables_section_title}: the input variables that you can change/tweak (trainable). + - {feedback_section_title}: the feedback about the code's execution result. + + If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. + """ + ) + + output_format_prompt_template = dedent( + """ + Output_format: Your output should be in the following XML/HTML format: + + ``` + {output_format} + ``` + + In <{reasoning_tag}>, explain the problem: 1. what the {instruction_section_title} means 2. what the {feedback_section_title} means to {variables_section_title} considering how {variables_section_title} follow {instruction_section_title}. 3. Reasoning about the suggested changes in {variables_section_title} (if needed) and the expected result. + + If you need to suggest a change in the values of {variables_section_title}, write down the suggested values in <{improved_variable_tag}>. Remember you can change only the values in {variables_section_title}, not others. When `type` of a variable is `code`, you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. + + If no changes are needed, just output TERMINATE. + """ + ) + + user_prompt_template = dedent( + """ + Now you see problem instance: + + ================================ + {problem_instance} + ================================ + + """ + ) + + final_prompt = dedent( + """ + What are your revised solutions on {names}? + + Your response: + """ + ) + + default_objective = "Propose a new solution that will improve the feedback." + + def __init__(self, *args, + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, + **kwargs): + optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() + super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, **kwargs) + self.buffer = [] + + def problem_instance(self, summary, mask=None): + mask = mask or [] + return ProblemInstance( + instruction=self.objective if "#Instruction" not in mask else "", + variables=( + self.repr_node_value_compact(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) + if self.optimizer_prompt_symbol_set.variables_section_title not in mask + else "" + ), + feedback=summary.user_feedback if self.optimizer_prompt_symbol_set.feedback_section_title not in mask else "", + optimizer_prompt_symbol_set=self.optimizer_prompt_symbol_set + ) + + def initialize_prompt(self): + self.representation_prompt = self.representation_prompt.format( + variable_expression_format=dedent(f""" + <{self.optimizer_prompt_symbol_set.variable_tag} name="variable_name" type="data_type"> + <{self.optimizer_prompt_symbol_set.value_tag}> + value + + <{self.optimizer_prompt_symbol_set.constraint_tag}> + constraint_expression + + + """), + value_tag=self.optimizer_prompt_symbol_set.value_tag, + variables_section_title=self.optimizer_prompt_symbol_set.variables_section_title.replace(" ", ""), + feedback_section_title=self.optimizer_prompt_symbol_set.feedback_section_title.replace(" ", ""), + instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), + ) + self.output_format_prompt = self.output_format_prompt_template.format( + output_format=self.optimizer_prompt_symbol_set.output_format, + reasoning_tag=self.optimizer_prompt_symbol_set.reasoning_tag, + improved_variable_tag=self.optimizer_prompt_symbol_set.improved_variable_tag, + instruction_section_title=self.optimizer_prompt_symbol_set.instruction_section_title.replace(" ", ""), + feedback_section_title=self.optimizer_prompt_symbol_set.feedback_section_title.replace(" ", ""), + variables_section_title=self.optimizer_prompt_symbol_set.variables_section_title.replace(" ", ""), + ) From 3fdc6883ce0a73574427dec3a99c5ff2d83e1f35 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 14 Aug 2025 20:47:10 +0000 Subject: [PATCH 149/172] Move priority_search under opto/features --- examples/priority_search_example.py | 2 +- opto/features/priority_search/__init__.py | 2 ++ .../algorithms => features}/priority_search/examples.py | 2 +- .../priority_search/priority_search.py | 4 ++-- .../priority_search/search_template.py | 0 .../{trainer/algorithms => features}/priority_search/utils.py | 0 opto/trainer/algorithms/__init__.py | 1 - opto/trainer/algorithms/priority_search/__init__.py | 2 -- tests/unit_tests/test_priority_search.py | 4 ++-- tests/unit_tests/test_sampler.py | 2 +- 10 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 opto/features/priority_search/__init__.py rename opto/{trainer/algorithms => features}/priority_search/examples.py (99%) rename opto/{trainer/algorithms => features}/priority_search/priority_search.py (99%) rename opto/{trainer/algorithms => features}/priority_search/search_template.py (100%) rename opto/{trainer/algorithms => features}/priority_search/utils.py (100%) delete mode 100644 opto/trainer/algorithms/priority_search/__init__.py diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index 4739ee0a..bd11e70f 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -3,7 +3,7 @@ from opto import trace from opto.utils.llm import LLM, LiteLLM from opto.optimizers import OptoPrimeV2 as OptoPrime -from opto.trainer.algorithms.priority_search import PrioritySearch as SearchAlgorithm +from opto.features.priority_search import PrioritySearch as SearchAlgorithm from opto.trainer.loggers import TensorboardLogger from opto.trainer.guide import VerbalJudgeGuide from typing import Any diff --git a/opto/features/priority_search/__init__.py b/opto/features/priority_search/__init__.py new file mode 100644 index 00000000..5ec28705 --- /dev/null +++ b/opto/features/priority_search/__init__.py @@ -0,0 +1,2 @@ +from opto.features.priority_search.priority_search import PrioritySearch +from opto.features.priority_search.examples import SequentialUpdate, SequentialSearch, BeamSearch \ No newline at end of file diff --git a/opto/trainer/algorithms/priority_search/examples.py b/opto/features/priority_search/examples.py similarity index 99% rename from opto/trainer/algorithms/priority_search/examples.py rename to opto/features/priority_search/examples.py index 90f6cb14..fedd5e1b 100644 --- a/opto/trainer/algorithms/priority_search/examples.py +++ b/opto/features/priority_search/examples.py @@ -1,5 +1,5 @@ -from opto.trainer.algorithms.priority_search import PrioritySearch +from opto.features.priority_search import PrioritySearch from typing import Union, Optional # Below we define several algorithms that use the PrioritySearch class. diff --git a/opto/trainer/algorithms/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py similarity index 99% rename from opto/trainer/algorithms/priority_search/priority_search.py rename to opto/features/priority_search/priority_search.py index 0a9f4aca..35342580 100644 --- a/opto/trainer/algorithms/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -7,8 +7,8 @@ from opto.trace.nodes import ParameterNode from opto.trainer.utils import async_run from opto.trainer.algorithms.basic_algorithms import batchify -from opto.trainer.algorithms.priority_search.search_template import SearchTemplate, Samples -from opto.trainer.algorithms.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict +from opto.features.priority_search.search_template import SearchTemplate, Samples +from opto.features.priority_search.utils import set_module_parameters, remap_update_dict, create_module_from_update_dict class ModuleCandidate: diff --git a/opto/trainer/algorithms/priority_search/search_template.py b/opto/features/priority_search/search_template.py similarity index 100% rename from opto/trainer/algorithms/priority_search/search_template.py rename to opto/features/priority_search/search_template.py diff --git a/opto/trainer/algorithms/priority_search/utils.py b/opto/features/priority_search/utils.py similarity index 100% rename from opto/trainer/algorithms/priority_search/utils.py rename to opto/features/priority_search/utils.py diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index 084cd459..2586fd31 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1,4 +1,3 @@ from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm -from opto.trainer.algorithms.priority_search import PrioritySearch \ No newline at end of file diff --git a/opto/trainer/algorithms/priority_search/__init__.py b/opto/trainer/algorithms/priority_search/__init__.py deleted file mode 100644 index caaf664f..00000000 --- a/opto/trainer/algorithms/priority_search/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch -from opto.trainer.algorithms.priority_search.examples import SequentialUpdate, SequentialSearch, BeamSearch \ No newline at end of file diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index c1bf703b..50602580 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -1,8 +1,8 @@ from opto import trace from opto.trainer.loader import DataLoader from opto.trainer.sampler import Sampler -from opto.trainer.algorithms.priority_search.priority_search import PrioritySearch as _PrioritySearch -from opto.trainer.algorithms.priority_search.priority_search import ModuleCandidate +from opto.features.priority_search.priority_search import PrioritySearch as _PrioritySearch +from opto.features.priority_search.priority_search import ModuleCandidate from opto.optimizers import OptoPrimeV2 from opto.trainer.guide import AutoGuide from opto.utils.llm import DummyLLM diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index fd9ceca4..ae53c8af 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -2,7 +2,7 @@ from opto.trainer.sampler import Sampler from opto.trainer.loader import DataLoader from opto.trainer.guide import AutoGuide -from opto.trainer.algorithms.priority_search.utils import is_node_copy +from opto.features.priority_search.utils import is_node_copy class Guide(AutoGuide): From 1200ba4aa7ec6c9c2284789cc3c47e45dfe0fbbb Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 14 Aug 2025 20:54:06 +0000 Subject: [PATCH 150/172] move sampler under opto/features/priority_search --- opto/{trainer => features/priority_search}/sampler.py | 0 opto/features/priority_search/search_template.py | 2 +- opto/features/priority_search/utils.py | 2 +- tests/unit_tests/test_priority_search.py | 2 +- tests/unit_tests/test_sampler.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename opto/{trainer => features/priority_search}/sampler.py (100%) diff --git a/opto/trainer/sampler.py b/opto/features/priority_search/sampler.py similarity index 100% rename from opto/trainer/sampler.py rename to opto/features/priority_search/sampler.py diff --git a/opto/features/priority_search/search_template.py b/opto/features/priority_search/search_template.py index d2b5e61c..5654d832 100644 --- a/opto/features/priority_search/search_template.py +++ b/opto/features/priority_search/search_template.py @@ -3,7 +3,7 @@ from opto import trace from opto.trainer.algorithms.basic_algorithms import Minibatch from opto.trainer.loader import DataLoader -from opto.trainer.sampler import Sampler, RolloutsGraph +from opto.features.priority_search.sampler import Sampler, RolloutsGraph # TODO save and load SearchTemplate # TODO async version??? diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index 8c4ed9db..c12e3ded 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -9,7 +9,7 @@ from opto.optimizers.utils import print_color from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify from opto.trainer.loader import DataLoader -from opto.trainer.sampler import Sampler, RolloutsGraph +from opto.features.priority_search.sampler import Sampler, RolloutsGraph import time # Some helper functions to convert between trace.Module and update_dict diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 50602580..6f5fa85b 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -1,6 +1,6 @@ from opto import trace from opto.trainer.loader import DataLoader -from opto.trainer.sampler import Sampler +from opto.features.priority_search.sampler import Sampler from opto.features.priority_search.priority_search import PrioritySearch as _PrioritySearch from opto.features.priority_search.priority_search import ModuleCandidate from opto.optimizers import OptoPrimeV2 diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index ae53c8af..2dc92439 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -1,5 +1,5 @@ from opto import trace -from opto.trainer.sampler import Sampler +from opto.features.priority_search.sampler import Sampler from opto.trainer.loader import DataLoader from opto.trainer.guide import AutoGuide from opto.features.priority_search.utils import is_node_copy From 7d1c3ddab36dc9328ed2e61584fb06c0fa25916f Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 15 Aug 2025 12:16:57 -0400 Subject: [PATCH 151/172] fix a few issues and now it works --- opto/optimizers/opro_v2.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 16254834..15e0d900 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -47,7 +47,7 @@ class ProblemInstance: variables: str feedback: str - optimizer_prompt_symbol_set: OptimizerPromptSymbolSet + optimizer_prompt_symbol_set: OPROPromptSymbolSet problem_template = dedent( """ @@ -92,8 +92,8 @@ class OPRO2(OptoPrimeV2): Specifically, a problem will be composed of the following parts: - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. - - {variables_section_title}: the input variables that you can change/tweak (trainable). - - {feedback_section_title}: the feedback about the code's execution result. + - {variables_section_title}: the proposed solution that you can change/tweak (trainable). + - {feedback_section_title}: the feedback about the solution. If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. """ @@ -134,14 +134,16 @@ class OPRO2(OptoPrimeV2): """ ) - default_objective = "Propose a new solution that will improve the feedback." + # Default Objective becomes instruction for the next block + default_objective = "Propose a new solution that will incorporate the feedback." def __init__(self, *args, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, **kwargs): optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, **kwargs) - self.buffer = [] + self.include_example = False # default example in OptoPrimeV2 does not work in OPRO + self.memory_size = 5 def problem_instance(self, summary, mask=None): mask = mask or [] From fa32419b6db63f79d2d689d97ef762b658aacee6 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 15 Aug 2025 12:37:16 -0400 Subject: [PATCH 152/172] add extraction test to both oprov2 and optoprimev2 --- opto/optimizers/opro_v2.py | 2 +- tests/llm_optimizers_tests/test_opro_v2.py | 164 ++++++++++++++++++ .../llm_optimizers_tests/test_optoprime_v2.py | 54 +++++- 3 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 tests/llm_optimizers_tests/test_opro_v2.py diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 15e0d900..23b8c767 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -85,7 +85,7 @@ def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: 1. think about how initial solution was generated... """ -class OPRO2(OptoPrimeV2): +class OPROv2(OptoPrimeV2): representation_prompt = dedent( """ You're tasked to change the proposed solution according to feedback. diff --git a/tests/llm_optimizers_tests/test_opro_v2.py b/tests/llm_optimizers_tests/test_opro_v2.py new file mode 100644 index 00000000..5eca4fe4 --- /dev/null +++ b/tests/llm_optimizers_tests/test_opro_v2.py @@ -0,0 +1,164 @@ +import os +import pytest +from opto.trace import bundle, node, GRAPH +import opto.optimizers +import importlib +import inspect +import json +import pickle +from opto.utils.llm import LLM + +from opto import trace +from opto.trace import node, bundle +from opto.optimizers.opro_v2 import OPROv2, OPROPromptSymbolSet + +# You can override for temporarly testing a specific optimizer ALL_OPTIMIZERS = [TextGrad] # [OptoPrimeMulti] ALL_OPTIMIZERS = [OptoPrime] + +# Skip tests if no API credentials are available +SKIP_REASON = "No API credentials found" +HAS_CREDENTIALS = os.path.exists("OAI_CONFIG_LIST") or os.environ.get("TRACE_LITELLM_MODEL") or os.environ.get( + "OPENAI_API_KEY") +llm = LLM() + +@pytest.fixture(autouse=True) +def clear_graph(): + """Reset the graph before each test""" + GRAPH.clear() + yield + GRAPH.clear() + + +@pytest.mark.skipif(not HAS_CREDENTIALS, reason=SKIP_REASON) +def test_response_extraction(): + pass + + +def test_tag_template_change(): + num_1 = node(1, trainable=True) + num_2 = node(2, trainable=True, description="<=5") + result = num_1 + num_2 + optimizer = OPROv2([num_1, num_2], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True, + optimizer_prompt_symbol_set=OPROPromptSymbolSet()) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + assert """""" in part1, "Expected tag to be present in part1" + assert """""" in part2, "Expected tag to be present in part2" + + print(part1) + print(part2) + + +@bundle() +def transform(num): + """Add number""" + return num + 1 + + +@bundle(trainable=True) +def multiply(num): + return num * 5 + + +def test_function_repr(): + num_1 = node(1, trainable=False) + + result = multiply(transform(num_1)) + optimizer = OPROv2([multiply.parameter], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + function_repr = """ + +def multiply(num): + return num * 5 + + +The code should start with: +def multiply(num): + +""" + + assert function_repr in part2, "Expected function representation to be present in part2" + +def test_big_data_truncation(): + num_1 = node(1, trainable=True) + + list_1 = node([1, 2, 3, 4, 5, 6, 7, 8, 9, 20] * 10, trainable=True) + + result = num_1 + list_1[30] + + optimizer = OPROv2([num_1, list_1], use_json_object_format=False, + ignore_extraction_error=False, initial_var_char_limit=10) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + truncated_repr = "[1, 2, 3, ...(skipped due to length limit)" + + assert truncated_repr in part2, "Expected truncated list representation to be present in part2" + +def test_extraction_pipeline(): + num_1 = node(1, trainable=True) + optimizer = OPROv2([num_1], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True) + + @bundle() + def propose_solution(x): + """ + Propose a solution to the given prompt using the input. + """ + return x + 1 + + result = propose_solution(num_1) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + messages = [ + {"role": "system", "content": part1}, + {"role": "user", "content": part2}, + ] + + # response = optimizer.llm(messages=messages) + # response = response.choices[0].message.content + + response = '```\n\n\nThe #Instruction requests a new solution that incorporates the given feedback into the proposed solution. The #Variables section includes an integer variable "int0" with the current value set to 1. The feedback states that this number should be made "bigger." Thus, the current value does not meet the feedback requirement, and I should change it to a larger integer value to comply with the feedback. A simple increment will suffice, so I will propose changing "int0" from 1 to 2.\n\n\nint0\n\n2\n\n\n\n```' + reasoning = response + suggestion = optimizer.extract_llm_suggestion(response) + + assert 'reasoning' in suggestion, "Expected 'reasoning' in suggestion" + assert 'variables' in suggestion, "Expected 'variables' in suggestion" + assert 'int0' in suggestion['variables'], "Expected 'int0' variable in suggestion" + assert suggestion['variables']['int0'] == 2, "Expected int0 to be incremented to 2" diff --git a/tests/llm_optimizers_tests/test_optoprime_v2.py b/tests/llm_optimizers_tests/test_optoprime_v2.py index af09c8b2..b1032f28 100644 --- a/tests/llm_optimizers_tests/test_optoprime_v2.py +++ b/tests/llm_optimizers_tests/test_optoprime_v2.py @@ -126,4 +126,56 @@ def test_big_data_truncation(): """ - assert truncated_repr in part2, "Expected truncated list representation to be present in part2" \ No newline at end of file + assert truncated_repr in part2, "Expected truncated list representation to be present in part2" + +def test_extraction_pipeline(): + num_1 = node(1, trainable=True) + num_2 = node(2, trainable=True, description="<=5") + result = num_1 + num_2 + optimizer = OptoPrimeV2([num_1, num_2], use_json_object_format=False, + ignore_extraction_error=False, + include_example=True, + optimizer_prompt_symbol_set=OptimizerPromptSymbolSet2()) + + optimizer.zero_feedback() + optimizer.backward(result, 'make this number bigger') + + summary = optimizer.summarize() + part1, part2 = optimizer.construct_prompt(summary) + + part1 = optimizer.replace_symbols(part1, optimizer.prompt_symbols) + part2 = optimizer.replace_symbols(part2, optimizer.prompt_symbols) + + messages = [ + {"role": "system", "content": part1}, + {"role": "user", "content": part2}, + ] + + # response = optimizer.llm(messages=messages) + # response = response.choices[0].message.content + response = """ +The instruction suggests that the output, `add0`, needs to be made bigger than it currently is (3). The code performs an addition of `int0` and `int1` to produce `add0`. To increase `add0`, we can increase the values of `int0` or `int1`, or both. Given that `int1` has a constraint of being less than or equal to 5, we can set `int0` to a higher value, since it has no explicit constraint. By adjusting `int0` to a higher value, the output can be made larger in accordance with the feedback. + + + +int0 + +5 + + + + +int1 + +5 + +""" + reasoning = response + suggestion = optimizer.extract_llm_suggestion(response) + + assert 'reasoning' in suggestion, "Expected 'reasoning' in suggestion" + assert 'variables' in suggestion, "Expected 'variables' in suggestion" + assert 'int0' in suggestion['variables'], "Expected 'int0' variable in suggestion" + assert 'int1' in suggestion['variables'], "Expected 'int1' variable in suggestion" + assert suggestion['variables']['int0'] == 5, "Expected int0 to be incremented to 5" + assert suggestion['variables']['int1'] == 5, "Expected int1 to be incremented to 5" From 26527f5e9d3cd289d3a19adadda6a62f2b2e5437 Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 15 Aug 2025 14:43:17 -0400 Subject: [PATCH 153/172] changed problem_instance symbol replace and updated init args --- opto/optimizers/opro_v2.py | 18 +++--------------- opto/optimizers/optoprime_v2.py | 21 ++------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 23b8c767..8c3b0711 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -63,22 +63,11 @@ class ProblemInstance: ) def __repr__(self) -> str: - return self.replace_symbols(self.problem_template.format( + return self.problem_template.format( instruction=self.instruction, variables=self.variables, feedback=self.feedback, - ), self.optimizer_prompt_symbol_set.default_prompt_symbols) - - def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: - default_prompt_symbols = { - "variables": "# Variables", - "feedback": "# Feedback", - "instruction": "# Problem Context", - } - - for k, v in symbols.items(): - text = text.replace(default_prompt_symbols[k], v) - return text + ) """ TODO: @@ -138,9 +127,8 @@ class OPROv2(OptoPrimeV2): default_objective = "Propose a new solution that will incorporate the feedback." def __init__(self, *args, - optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OPROPromptSymbolSet(), **kwargs): - optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, **kwargs) self.include_example = False # default example in OptoPrimeV2 does not work in OPRO self.memory_size = 5 diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index db651bfb..0ec5aa48 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -294,7 +294,7 @@ class ProblemInstance: ) def __repr__(self) -> str: - return self.replace_symbols(self.problem_template.format( + return self.problem_template.format( instruction=self.instruction, code=self.code, documentation=self.documentation, @@ -303,24 +303,7 @@ def __repr__(self) -> str: outputs=self.outputs, others=self.others, feedback=self.feedback, - ), self.optimizer_prompt_symbol_set.default_prompt_symbols) - - def replace_symbols(self, text: str, symbols: Dict[str, str]) -> str: - default_prompt_symbols = { - "variables": "# Variables", - "constraints": "# Constraints", - "inputs": "# Inputs", - "outputs": "# Outputs", - "others": "# Others", - "feedback": "# Feedback", - "instruction": "# Instruction", - "code": "# Code", - "documentation": "# Documentation", - } - - for k, v in symbols.items(): - text = text.replace(default_prompt_symbols[k], v) - return text + ) class OptoPrimeV2(OptoPrime): From f0acd6a4108ef62d5469183df921d20d9da3e59e Mon Sep 17 00:00:00 2001 From: windweller Date: Fri, 15 Aug 2025 16:01:29 -0400 Subject: [PATCH 154/172] update the default parameter in OPROv2 --- opto/optimizers/opro_v2.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 8c3b0711..3b66c14a 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -127,11 +127,14 @@ class OPROv2(OptoPrimeV2): default_objective = "Propose a new solution that will incorporate the feedback." def __init__(self, *args, - optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = OPROPromptSymbolSet(), + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet = None, + include_example=False, # default example in OptoPrimeV2 does not work in OPRO + memory_size=5, **kwargs): - super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, **kwargs) - self.include_example = False # default example in OptoPrimeV2 does not work in OPRO - self.memory_size = 5 + optimizer_prompt_symbol_set = optimizer_prompt_symbol_set or OPROPromptSymbolSet() + super().__init__(*args, optimizer_prompt_symbol_set=optimizer_prompt_symbol_set, + include_example=include_example, memory_size=memory_size, + **kwargs) def problem_instance(self, summary, mask=None): mask = mask or [] From 0936a5af5ea5ebd901dd8ceac4ee912ba081d8a6 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 15 Aug 2025 20:07:19 +0000 Subject: [PATCH 155/172] Rename AutoGuide -> Guide, VerbalJudgeGuide -> LLMJudge, model_dump -> export --- examples/gsm8k_trainer_example.py | 6 +- .../run_bigbench_trace_async.py | 64 +++++++------- examples/priority_search_example.py | 4 +- examples/search_algo_example.py | 84 +++++++++---------- opto/features/priority_search/sampler.py | 8 +- opto/trace/modules.py | 4 +- opto/trainer/algorithms/algorithm.py | 6 +- opto/trainer/guide.py | 4 +- opto/trainer/utils.py | 48 +++++------ tests/unit_tests/test_batch_run.py | 32 +++---- tests/unit_tests/test_modules.py | 26 +++--- tests/unit_tests/test_priority_search.py | 4 +- tests/unit_tests/test_sampler.py | 4 +- tests/unit_tests/test_saving_loading.py | 4 +- 14 files changed, 149 insertions(+), 149 deletions(-) diff --git a/examples/gsm8k_trainer_example.py b/examples/gsm8k_trainer_example.py index 7b627674..dd87b749 100644 --- a/examples/gsm8k_trainer_example.py +++ b/examples/gsm8k_trainer_example.py @@ -5,7 +5,7 @@ from opto.optimizers import OptoPrime from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm from opto.trainer.loggers import TensorboardLogger -from opto.trainer.guide import VerbalJudgeGuide +from opto.trainer.guide import LLMJudge from typing import Any @@ -46,7 +46,7 @@ def forward(self, message: Any) -> Any: return self.model(self.system_prompt, self.user_prompt_template, message) -Guide = VerbalJudgeGuide +Guide = LLMJudge Logger = TensorboardLogger @@ -80,7 +80,7 @@ def main(): agent=agent, optimizer=optimizer, logger=logger) - + alg.train(guide, train_dataset, num_epochs=num_epochs, diff --git a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py index 3688907f..8211139c 100644 --- a/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py +++ b/examples/minibatch_bbh_aynsc/run_bigbench_trace_async.py @@ -11,7 +11,7 @@ import pickle import os from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, evaluate -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide def eval_metric(true, prediction): @@ -28,24 +28,24 @@ def eval_metric(true, prediction): return prediction == true -class BigBenchGuide(AutoGuide): +class BigBenchGuide(Guide): """ Custom guide that uses the eval_metric function to evaluate responses and provide feedback for the BigBench tasks. """ - + def __init__(self): super().__init__() - + def forward(self, task, response, info, **kwargs): """ Evaluate the response using the eval_metric function. - + Args: task: The question response: The model's answer info: The correct answer - + Returns: score: 1.0 if correct, 0.0 if incorrect feedback: Feedback message @@ -53,25 +53,25 @@ def forward(self, task, response, info, **kwargs): try: correctness = eval_metric(info, response) score = 1.0 if correctness else 0.0 - + if correctness: feedback = "The answer is correct! No need to change anything." else: feedback = f"The answer is wrong. We expect the output of your answer to be \"{info}\". Please modify the prompt and relevant parts of the program to help LLM produce the right answer." - + return score, feedback except Exception as e: return 0.0, f"Error occurred: {str(e)}. Please fix the error and try again." - + def metric(self, task, response, info, **kwargs): """ Evaluate the response and return just the score. - + Args: task: The question response: The model's answer info: The correct answer - + Returns: score: 1.0 if correct, 0.0 if incorrect """ @@ -88,14 +88,14 @@ def __init__(self): self.prompt_template = dedent( """ Given the fields `question`, produce the fields `answer`. - + --- - + Follow the following format. - - Question: - Answer: - + + Question: + Answer: + --- Question: {} Answer: @@ -216,7 +216,7 @@ def forward(self, question): def learn_predict(dp, optimizer, examples, val_examples, task_name, save_dir): """ Train the model using the MinibatchUpdate algorithm. - + Args: dp: The model to train optimizer: The optimizer to use @@ -224,33 +224,33 @@ def learn_predict(dp, optimizer, examples, val_examples, task_name, save_dir): val_examples: Validation examples task_name: Name of the task save_dir: Directory to save checkpoints - + Returns: dp: The trained model rewards: The final validation accuracy """ # Create the guide guide = BigBenchGuide() - + # Prepare the training dataset train_dataset = { 'inputs': [ex['question'] for ex in examples], 'infos': [ex['answer'] for ex in examples] } - + # Prepare the validation dataset val_dataset = { 'inputs': [ex['question'] for ex in val_examples], 'infos': [ex['answer'] for ex in val_examples] } - + # Create the MinibatchUpdate algorithm algorithm = MinibatchAlgorithm( agent=dp, optimizer=optimizer, num_threads=4 # Adjust as needed ) - + # Train the model train_score, val_score = algorithm.train( guide=guide, @@ -265,30 +265,30 @@ def learn_predict(dp, optimizer, examples, val_examples, task_name, save_dir): verbose=True, min_score=None # No minimum score required ) - + return dp, val_score def evaluate_dp(dp, examples): """ Evaluate the model on a set of examples using MinibatchAlgorithm's evaluate method. - + Args: dp: The model to evaluate examples: The examples to evaluate on - + Returns: accuracy: The accuracy of the model responses: The responses of the model """ - + # Create the guide guide = BigBenchGuide() - + # Prepare the evaluation dataset inputs = [ex['question'] for ex in examples] infos = [ex['answer'] for ex in examples] - + # Use the evaluate function from basic_algorithm.py scores = evaluate( agent=dp, @@ -299,10 +299,10 @@ def evaluate_dp(dp, examples): num_threads=4, # Adjust as needed description=f"Evaluating on {len(examples)} examples" # Add descriptive message for the progress bar ) - + # Calculate accuracy accuracy = np.mean(scores) if scores else 0.0 - + # Collect responses for analysis responses = [] for example in tqdm(examples): @@ -312,7 +312,7 @@ def evaluate_dp(dp, examples): except Exception as e: print(f"Error during evaluation: {str(e)}") responses.append(None) - + return accuracy, responses diff --git a/examples/priority_search_example.py b/examples/priority_search_example.py index bd11e70f..caf03cbc 100644 --- a/examples/priority_search_example.py +++ b/examples/priority_search_example.py @@ -5,7 +5,7 @@ from opto.optimizers import OptoPrimeV2 as OptoPrime from opto.features.priority_search import PrioritySearch as SearchAlgorithm from opto.trainer.loggers import TensorboardLogger -from opto.trainer.guide import VerbalJudgeGuide +from opto.trainer.guide import LLMJudge from typing import Any @@ -46,7 +46,7 @@ def forward(self, message: Any) -> Any: return self.model(self.system_prompt, self.user_prompt_template, message) -Guide = VerbalJudgeGuide +Guide = LLMJudge Logger = TensorboardLogger diff --git a/examples/search_algo_example.py b/examples/search_algo_example.py index 14fc61ea..e40cfa7e 100644 --- a/examples/search_algo_example.py +++ b/examples/search_algo_example.py @@ -16,7 +16,7 @@ from opto.trainer.algorithms.basic_algorithms import MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide from opto.trainer.loggers import DefaultLogger from opto.utils.llm import LLM @@ -26,13 +26,13 @@ @trace.model class Learner(Module): """A basic LLM Agent for solving math problems.""" - - def __init__(self, + + def __init__(self, system_prompt: str = "You're a helpful agent answering math problems.", user_prompt_template: str = "Solve the following math problem step-by-step: {message}", llm: LLM = None): """Initialize the learner agent. - + Args: system_prompt: System prompt to guide LLM behavior user_prompt_template: Template for formatting user messages @@ -46,11 +46,11 @@ def __init__(self, @trace.bundle() def call_llm(self, system_prompt: str, user_prompt: str) -> str: """Call LLM model with the given prompts. - + Args: system_prompt: The system prompt user_prompt: The user prompt - + Returns: The LLM response content """ @@ -64,23 +64,23 @@ def call_llm(self, system_prompt: str, user_prompt: str) -> str: def forward(self, message: Any) -> str: """Agent's forward pass to process a message. - + Args: message: The input message to process - + Returns: The generated response - """ + """ user_prompt = self.user_prompt_template.format(message=message) return self.call_llm(self.system_prompt, user_prompt) -class TeacherGuide(AutoGuide): +class TeacherGuide(Guide): """Guide that uses LLM to judge answers and provide feedback.""" - + def __init__(self, model: str = "gpt-4o-mini"): """Initialize the teacher guide. - + Args: model: The LLM model to use for evaluation """ @@ -112,13 +112,13 @@ def __init__(self, model: str = "gpt-4o-mini"): def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[float, str]: """Get feedback on a student response. - + Args: task: The original math problem response: The student's answer info: The reference/correct answer **kwargs: Additional arguments - + Returns: Tuple of (score, feedback_text) """ @@ -140,16 +140,16 @@ def get_feedback(self, task: str, response: str, info: Any, **kwargs) -> Tuple[f return 1.0, "Correct." else: return 0.0, f"Incorrect. Feedback: {feedback_text}" - + def metric(self, task: str, content: str, info: Any, **kwargs) -> float: """Calculate the metric score for an answer. - + Args: task: The original math problem content: The student's answer info: The reference/correct answer **kwargs: Additional arguments - + Returns: Score (0.0 or 1.0) """ @@ -159,10 +159,10 @@ def metric(self, task: str, content: str, info: Any, **kwargs) -> float: class SimpleLogger(DefaultLogger): """Simplified logger that only shows important metrics.""" - + def log(self, name: str, data: Any, step: int, **kwargs): """Log only specific metrics to reduce output clutter. - + Args: name: The name of the metric data: The metric value @@ -174,7 +174,7 @@ def log(self, name: str, data: Any, step: int, **kwargs): 'Average test score', 'Validation score' ] - + if name in important_metrics or 'Parameter' in name: super().log(name, data, step, **kwargs) @@ -182,12 +182,12 @@ def log(self, name: str, data: Any, step: int, **kwargs): def main(): """Run the main training process with command line arguments.""" parser = argparse.ArgumentParser(description='Train agent using various algorithms') - + # Algorithm parameters parser.add_argument('--algorithm_type', type=str, default='UCBsearch', choices=['minibatch', 'basicsearch', 'beamsearch', 'beamsearchhistory', 'UCBsearch'], help='Type of algorithm to use') - + # Dataset parameters parser.add_argument('--dataset', type=str, default='xuanfeiren/math_hard_gemini', help='Dataset to use for training') @@ -197,7 +197,7 @@ def main(): help='Number of validation samples') parser.add_argument('--num_test_samples', type=int, default=20, help='Number of test samples') - + # LLM Model parameters parser.add_argument('--trace_model', type=str, default=None, help='Model to use for trace operations') @@ -205,7 +205,7 @@ def main(): help='Model to use for student agent') parser.add_argument('--teacher_model', type=str, default=None, help='Model to use for teacher guide') - + # Training parameters parser.add_argument('--num_epochs', type=int, default=1, help='Number of training epochs') @@ -219,7 +219,7 @@ def main(): help='How often to log results') parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility') - + # Algorithm-specific parameters parser.add_argument('--beam_width', type=int, default=3, help='Beam width for beam search algorithms') @@ -233,7 +233,7 @@ def main(): help='Maximum history size for history-based algorithms') parser.add_argument('--num_basicsearch_proposals', type=int, default=2, help='Number of proposals for basic search algorithm') - + # UCB algorithm-specific parameters parser.add_argument('--max_buffer_size', type=int, default=5, help='Maximum buffer size for UCB algorithms') @@ -245,16 +245,16 @@ def main(): help='Training batch size for UCB algorithms') parser.add_argument('--evaluation_batch_size', type=int, default=20, help='Evaluation batch size for UCB algorithms') - + args = parser.parse_args() - + # Set environment variables if args.trace_model: os.environ["TRACE_LITELLM_MODEL"] = args.trace_model # Set random seed np.random.seed(args.seed) - + # Check for API Keys if not os.getenv("OPENAI_API_KEY") and not os.getenv("ANTHROPIC_API_KEY"): print_color("Warning: OPENAI_API_KEY or ANTHROPIC_API_KEY environment variables not found. LLM calls may fail.", "red") @@ -262,7 +262,7 @@ def main(): # Load and prepare data print(f"Loading data from {args.dataset}...") math_data = datasets.load_dataset(args.dataset) - + # Select data subsets train_data = math_data['train'].select( range(args.num_train_samples, args.num_train_samples + args.num_validate_samples) @@ -274,7 +274,7 @@ def main(): train_dataset = {'inputs': train_data['problem'], 'infos': train_data['solution']} validate_dataset = {'inputs': validate_data['problem'], 'infos': validate_data['solution']} test_dataset = {'inputs': test_data['problem'], 'infos': test_data['solution']} - + # Log dataset sizes print(f"Training samples: {len(train_dataset['inputs'])}") print(f"Validation samples: {len(validate_dataset['inputs'])}") @@ -290,7 +290,7 @@ def main(): optimizer = OptoPrime(agent.parameters()) logger = SimpleLogger() - + # Create algorithm if args.algorithm_type == 'minibatch': algorithm = MinibatchAlgorithm( @@ -331,7 +331,7 @@ def main(): ) else: raise ValueError(f"Unknown algorithm type: {args.algorithm_type}") - + # Prepare training parameters train_params = { "guide": train_guide, @@ -346,7 +346,7 @@ def main(): "log_frequency": args.log_frequency, "validation_dataset_size": args.validation_dataset_size, } - + # Add algorithm-specific parameters if args.algorithm_type in ['beamsearch', 'beamsearchhistory']: train_params.update({ @@ -354,33 +354,33 @@ def main(): "num_proposals": args.num_basicsearch_proposals, "max_depth": args.max_depth }) - + if args.algorithm_type == 'beamsearchhistory': train_params["max_history_size"] = args.max_history_size - + elif args.algorithm_type == 'basicsearch': train_params["num_proposals"] = args.num_basicsearch_proposals - + elif args.algorithm_type == 'UCBsearch': train_params.update({ "num_search_iterations": args.num_search_iterations, "train_batch_size": args.train_batch_size_ucb, "evaluation_batch_size": args.evaluation_batch_size }) - + # Start training print(f"Training with {args.algorithm_type} algorithm...") start_time = time.time() metrics, final_score = algorithm.train(**train_params) duration = time.time() - start_time print(f"Training complete, time taken: {duration:.2f} seconds") - + # Print metrics summary based on algorithm type if args.algorithm_type in ['beamsearch', 'beamsearchhistory'] and 'best_validation_scores' in metrics: print("\nBest validation scores at each depth:") for depth, score in enumerate(metrics['best_validation_scores']): print(f" Depth {depth+1}: {score:.4f}") - + elif args.algorithm_type == 'UCBsearch': print("\nUCB Algorithm Metrics:") if 'best_candidate_scores' in metrics and metrics['best_candidate_scores']: @@ -388,9 +388,9 @@ def main(): print(f" Final best candidate score: {metrics['best_candidate_scores'][-1]:.4f}") if 'buffer_avg_score' in metrics and metrics['buffer_avg_score']: print(f" Final buffer average score: {metrics['buffer_avg_score'][-1]:.4f}") - + print(f"Final score: {final_score:.4f}") - + return metrics, final_score diff --git a/opto/features/priority_search/sampler.py b/opto/features/priority_search/sampler.py index 3d46ea05..ce35f736 100644 --- a/opto/features/priority_search/sampler.py +++ b/opto/features/priority_search/sampler.py @@ -4,7 +4,7 @@ from typing import Union, List, Tuple, Dict, Any, Optional from opto import trace from opto.trainer.utils import batch_run -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide @dataclass class Rollout: @@ -88,8 +88,8 @@ def __init__(self, raise TypeError("xs must be a list.") if not isinstance(infos, list): raise TypeError("infos must be a list.") - if not isinstance(guide, AutoGuide): - raise TypeError("guide must be a AutoGuide.") + if not isinstance(guide, Guide): + raise TypeError("guide must be a Guide.") if len(xs) != len(infos): raise ValueError("Length of xs must match length of infos.") self.module = module @@ -189,7 +189,7 @@ def __init__(self, loader, guide, num_threads=1, sub_batch_size=None, forward=No Args: loader (DataLoader): The data loader to sample from. - guide (AutoGuide): The guide to evaluate the proposals. + guide (Guide): The guide to evaluate the proposals. num_threads (int): Number of threads to use for sampling. sub_batch_size (int, optional): Size of the sub-batch to use for sampling. If None, uses the batch size. score_range (tuple): The range of scores to consider valid. diff --git a/opto/trace/modules.py b/opto/trace/modules.py index 9310c2ff..f08d0165 100644 --- a/opto/trace/modules.py +++ b/opto/trace/modules.py @@ -17,7 +17,7 @@ def model(cls): class ModelWrapper(cls, Module): - def model_dump(self, filename, projections: Optional[List[Projection]] = None): + def export(self, filename, projections: Optional[List[Projection]] = None): """Dump the model's source code to a file, including all methods and attributes. Ignores dunder methods unless they were overridden by the user. """ @@ -48,7 +48,7 @@ def model_dump(self, filename, projections: Optional[List[Projection]] = None): # For dunder methods, check if they were overridden try: print(cls.__name__, "<>", member.__qualname__) - # MixedClass <> test_model_dump_mixed_trainable..MixedClass.__init__ + # MixedClass <> test_export_mixed_trainable..MixedClass.__init__ # if we wrap it inside a function, the qualname is different than when we dont if hasattr(member, '__qualname__') and cls.__name__ in member.__qualname__: filtered_members.append((name, member)) diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index b3506e23..25180ef9 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -2,7 +2,7 @@ from opto.trace.modules import Module from opto.trainer.loggers import DefaultLogger from opto.trainer.loader import DataLoader -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide from opto.optimizers.optimizer import Optimizer import os import pickle @@ -102,7 +102,7 @@ def save(self, path: str): _path = path+ f"_{key}.module" value.save(_path) d[key] = _path - elif isinstance(value, AutoGuide): + elif isinstance(value, Guide): _path = path + f"_{key}.guide" value.save(_path) d[key] = _path @@ -135,7 +135,7 @@ def load(self, path: str): assert isinstance(attr, Module), f"Expected {key} to be a Module, got {type(attr)}" elif value.endswith('.guide'): attr = self.__dict__[key] - assert isinstance(attr, AutoGuide), f"Expected {key} to be an AutoGuide, got {type(attr)}" + assert isinstance(attr, Guide), f"Expected {key} to be an Guide, got {type(attr)}" elif value.endswith('.dataloader'): attr = self.__dict__[key] assert isinstance(attr, DataLoader), f"Expected {key} to be a DataLoader, got {type(attr)}" diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index df465cc8..53f225ca 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -10,7 +10,7 @@ def exact_match_metric(question, student_answer, info): """ Exact match metric """ return float(student_answer == info) -class AutoGuide: +class Guide: """ Base class for all guides that provide feedback on content. @@ -69,7 +69,7 @@ def load(self, path: str): setattr(self, key, value) -class VerbalJudgeGuide(AutoGuide): +class LLMJudge(Guide): """ This is a combined metric + feedback guide that asks LLM to provide a binary judgment (True/False) and then if False, provide feedback. diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index 6f7ccc15..ffb6b999 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -5,7 +5,7 @@ from tqdm.asyncio import tqdm_asyncio from opto.trace.bundle import ALLOW_EXTERNAL_DEPENDENCIES from opto.trace.modules import Module -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, description = None, allow_sequential_run=True): """Run multiple functions in asynchronously. @@ -36,13 +36,13 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de if (max_workers == 1) and allow_sequential_run: # run without asyncio print(f"{description} (Running sequentially).") return [run(*args, **kwargs) for run, args, kwargs in zip(runs, args_list, kwargs_list)] - else: + else: async def _run(): loop = asyncio.get_event_loop() with ThreadPoolExecutor(max_workers=max_workers) as executor: - tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) + tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) for run, args, kwargs, in zip(runs, args_list, kwargs_list)] - + # Use the description in the tqdm progress bar if provided if description: return await tqdm_asyncio.gather(*tasks, desc=description) @@ -54,11 +54,11 @@ async def _run(): def batch_run(max_workers=None, description=None): """ Create a function that runs in parallel using asyncio, with support for batching. - The batch size is inferred as the length of the longest argument or keyword argument. + The batch size is inferred as the length of the longest argument or keyword argument. Args: fun (callable): The function to run. - + max_workers (int, optional): Maximum number of worker threads to use. If None, the default ThreadPoolExecutor behavior is used. description (str, optional): Description to display in the progress bar. @@ -66,9 +66,9 @@ def batch_run(max_workers=None, description=None): Returns: callable: A new function that processes batches of inputs. - NOTE: - If fun takes input that has __len__ (like lists or arrays), they won't be broadcasted. - When using batch_run, be sure to pass list of such arguments of the same length. + NOTE: + If fun takes input that has __len__ (like lists or arrays), they won't be broadcasted. + When using batch_run, be sure to pass list of such arguments of the same length. Example: >>> @batch_run(max_workers=4, description="Processing batch") @@ -78,33 +78,33 @@ def batch_run(max_workers=None, description=None): >>> y = 10 >>> outputs = my_function(x, y) >>> # outputs will be [11, 12, 13, 14, 15] - >>> # This will run the function in asynchronously with 4 threads + >>> # This will run the function in asynchronously with 4 threads """ - + def decorator(fun): """ Decorator to create a function that runs in parallel using asyncio, with support for batching. - + Args: fun (callable): The function to run. - + max_workers (int, optional): Maximum number of worker threads to use. If None, the default ThreadPoolExecutor behavior is used. description (str, optional): Description to display in the progress bar. Returns: callable: A new function that processes batches of inputs. - """ + """ def _fun(*args, **kwargs): - + # We try to infer the batch size from the args all_args = args + tuple(kwargs.values()) # find all list or array-like arguments and use their length as batch size batch_size = max(len(arg) for arg in all_args if hasattr(arg, '__len__')) - + # broadcast the batch size to all args and record the indices that are broadcasted args = [arg if hasattr(arg, '__len__') else [arg] * batch_size for arg in args] - kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} + kwargs = {k: v if hasattr(v, '__len__') else [v] * batch_size for k, v in kwargs.items()} # assert that all args and kwargs have the same length lengths = [len(arg) for arg in args] + [len(v) for v in kwargs.values()] @@ -113,10 +113,10 @@ def _fun(*args, **kwargs): # deepcopy if it is a trace.Module (as they may have mutable state) # Module.copy() is used to create a new instance with the same parameters - _args = [[a.copy() if isinstance(a, (Module, AutoGuide)) else a for a in arg ] for arg in args ] - _kwargs = {k: [a.copy() if isinstance(a, (Module, AutoGuide)) else a for a in v ] for k, v in kwargs.items() } + _args = [[a.copy() if isinstance(a, (Module, Guide)) else a for a in arg ] for arg in args ] + _kwargs = {k: [a.copy() if isinstance(a, (Module, Guide)) else a for a in v ] for k, v in kwargs.items() } - # Run the forward function in parallel using asyncio with the same parameters. + # Run the forward function in parallel using asyncio with the same parameters. # Since trace.Node is treated as immutable, we can safely use the same instance. # The resultant graph will be the same as if we had called the function with the original arguments. @@ -145,25 +145,25 @@ def tester(t): # regular time-consuming function args_list = [(3,), (3,), (2,), (3,), (3,), (2,), (2,), (3,), (2,), (3,)] kwargs_list = [{}] * 10 import time - + # Example with 1 thread (runs sequentially) print("Running with 1 thread (sequential):") start = time.time() output = async_run(runs, args_list, kwargs_list, max_workers=1) print(f"Time with 1 thread: {time.time()-start:.2f} seconds") - + # Example with limited workers (2 threads) print("\nRunning with 2 threads (parallel):") start = time.time() output = async_run(runs, args_list, kwargs_list, max_workers=2) print(f"Time with 2 threads: {time.time()-start:.2f} seconds") - + # Example with limited workers (4 threads) print("\nRunning with 4 threads (parallel):") start = time.time() output = async_run(runs, args_list, kwargs_list, max_workers=4) print(f"Time with 4 threads: {time.time()-start:.2f} seconds") - + # Example with default number of workers print("\nRunning with default number of threads:") start = time.time() diff --git a/tests/unit_tests/test_batch_run.py b/tests/unit_tests/test_batch_run.py index 5da10ddb..daf983c3 100644 --- a/tests/unit_tests/test_batch_run.py +++ b/tests/unit_tests/test_batch_run.py @@ -22,10 +22,10 @@ def fun(x: List[int], y: List[int]) -> List[int]: return [a + b for a, b in zip(x, y)] x = [[1, 2, 3], [4, 5, 6]] - y = [10, 20, 30] # list won't be braodcasted correctly + y = [10, 20, 30] # list won't be braodcasted correctly raise_error = False - try: + try: outputs = fun(x, y) except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" @@ -38,9 +38,9 @@ def fun(x: List[int], y: List[int]) -> List[int]: assert outputs == [[11, 22, 33], [14, 25, 36]], f"Expected [[11, 22, 33], [14, 25, 36]], got {outputs}" # This will raise an error because x and y have different lengths - # y = [10, 20] + # y = [10, 20] # outputs = fun(x, y) - + def test_batch_run_module(): @@ -49,12 +49,12 @@ class MyModule: def __init__(self, param): self.param = trace.node(param, trainable=True) self._state = 0 - + def forward(self, x): y = x + self.param self._state += 1 # This should not affect the batch run return y - + module = MyModule(10) x = [1, 2, 3, 4, 5] outputs = batch_run(max_workers=3)(module.forward)(x) @@ -67,7 +67,7 @@ def forward(self, x): y = [10, 20, 30, 40, 50, 60] # This should raise an error because x and y have different lengths raise_error = False - try: + try: outputs = batch_run(max_workers=3)(module.forward)(x, y) except ValueError as e: assert str(e) == "All arguments and keyword arguments must have the same length.", f"Unexpected error: {e}" @@ -75,40 +75,40 @@ def forward(self, x): assert raise_error, "Expected a ValueError but did not get one." -def test_evaluate(): +def test_evaluate(): # This test the evaluate function in opto.trainer.evaluators built on top of batch_run from opto.trainer.evaluators import evaluate - from opto.trainer.guide import AutoGuide + from opto.trainer.guide import Guide from opto import trace @trace.model class MyAgent: def __init__(self, param): - self.param = trace.node(param, trainable=True) - + self.param = trace.node(param, trainable=True) + def forward(self, x): y = x + self.param self.param += 1 # This should not affect the batch run return y - - class MyGuide(AutoGuide): + + class MyGuide(Guide): def __init__(self, param): super().__init__() self.param = param def get_feedback(self, query, response, reference=None): score = float(response == query + self.param + reference) - feedback = f"Score: {score}, Response: {response}, Query: {query}" + feedback = f"Score: {score}, Response: {response}, Query: {query}" self.param += 1 # This should not affect the batch run return score, feedback - + agent = MyAgent(10) guide = MyGuide(10) inputs = [1, 2, 3, 4, 5] infos = [0, 1, 2, 3, 4] # These are the expected outputs (query + param + info) evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=1, num_threads=1) expected_scores = [1, 0, 0, 0, 0] # All inputs should match the expected outputs - assert (evaluated_scores == expected_scores).all(), f"Expected {expected_scores}, got {evaluated_scores}" + assert (evaluated_scores == expected_scores).all(), f"Expected {expected_scores}, got {evaluated_scores}" evaluated_scores = evaluate(agent, guide, inputs, infos, num_samples=2, num_threads=1) diff --git a/tests/unit_tests/test_modules.py b/tests/unit_tests/test_modules.py index 7e93f049..f5a5d6cc 100644 --- a/tests/unit_tests/test_modules.py +++ b/tests/unit_tests/test_modules.py @@ -157,7 +157,7 @@ def test_multiple_inheritance(): assert result._data == 2 -# Test cases for model_dump +# Test cases for export @model class DummyClass: def __init__(self): @@ -189,12 +189,12 @@ def complex_method(self, x): def __str__(self): return "ComplexClass" -def test_model_dump_basic(): +def test_export_basic(): dummy = DummyClass() dummy._param._data = 42 # Change the node value temp_file = "temp_dummy.py" try: - dummy.model_dump(temp_file) + dummy.export(temp_file) with open(temp_file, "r") as f: content = f.read() # Check if class definition is present @@ -214,11 +214,11 @@ def test_model_dump_basic(): if os.path.exists(temp_file): os.remove(temp_file) -def test_model_dump_complex(): +def test_export_complex(): complex_obj = ComplexClass() temp_file = "temp_complex.py" try: - complex_obj.model_dump(temp_file) + complex_obj.export(temp_file) with open(temp_file, "r") as f: content = f.read() # Check if class definition is present @@ -233,13 +233,13 @@ def test_model_dump_complex(): if os.path.exists(temp_file): os.remove(temp_file) -def test_model_dump_with_projection(): +def test_export_with_projection(): dummy = DummyClass() temp_file = "temp_dummy_formatted.py" try: # Test with BlackCodeFormatter from opto.trace.projections import BlackCodeFormatter - dummy.model_dump(temp_file, projections=[BlackCodeFormatter()]) + dummy.export(temp_file, projections=[BlackCodeFormatter()]) with open(temp_file, "r") as f: content = f.read() # Check if content is properly formatted @@ -265,13 +265,13 @@ def non_trainable_method(self, x): def another_non_trainable(self, y): return y + 1 -def test_model_dump_non_trainable(): +def test_export_non_trainable(): obj = NonTrainableClass() obj._param._data = 10 # Change node value obj._param2._data = 20 # Change another node value temp_file = "temp_non_trainable.py" try: - obj.model_dump(temp_file) + obj.export(temp_file) with open(temp_file, "r") as f: content = f.read() # Check if class definition is present @@ -292,7 +292,7 @@ def test_model_dump_non_trainable(): if os.path.exists(temp_file): os.remove(temp_file) -def test_model_dump_mixed_trainable(): +def test_export_mixed_trainable(): @model class MixedClass: @@ -319,7 +319,7 @@ def non_trainable_method(self, y): temp_file = "temp_mixed.py" try: - obj.model_dump(temp_file) + obj.export(temp_file) with open(temp_file, "r") as f: content = f.read() # Check if class definition is present @@ -341,7 +341,7 @@ def non_trainable_method(self, y): if os.path.exists(temp_file): os.remove(temp_file) -def test_model_dump_and_import(): +def test_export_and_import(): @model class StrangeCalculator: def __init__(self): @@ -369,7 +369,7 @@ def multiply(self, x, y): # Dump the model temp_file = "temp_calculator.py" try: - calc.model_dump(temp_file) + calc.export(temp_file) # Import the dumped class import importlib.util diff --git a/tests/unit_tests/test_priority_search.py b/tests/unit_tests/test_priority_search.py index 6f5fa85b..2ebda047 100644 --- a/tests/unit_tests/test_priority_search.py +++ b/tests/unit_tests/test_priority_search.py @@ -4,7 +4,7 @@ from opto.features.priority_search.priority_search import PrioritySearch as _PrioritySearch from opto.features.priority_search.priority_search import ModuleCandidate from opto.optimizers import OptoPrimeV2 -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide from opto.utils.llm import DummyLLM import re @@ -12,7 +12,7 @@ import copy -class Guide(AutoGuide): +class Guide(Guide): def get_feedback(self, query, response, reference=None, **kwargs): """ diff --git a/tests/unit_tests/test_sampler.py b/tests/unit_tests/test_sampler.py index 2dc92439..c1a70fdb 100644 --- a/tests/unit_tests/test_sampler.py +++ b/tests/unit_tests/test_sampler.py @@ -1,11 +1,11 @@ from opto import trace from opto.features.priority_search.sampler import Sampler from opto.trainer.loader import DataLoader -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide from opto.features.priority_search.utils import is_node_copy -class Guide(AutoGuide): +class Guide(Guide): def get_feedback(self, query, response, reference=None, **kwargs): """ diff --git a/tests/unit_tests/test_saving_loading.py b/tests/unit_tests/test_saving_loading.py index c0444512..06f09a54 100644 --- a/tests/unit_tests/test_saving_loading.py +++ b/tests/unit_tests/test_saving_loading.py @@ -4,7 +4,7 @@ from opto.trainer.loader import DataLoader from opto.trainer.algorithms import BasicSearchAlgorithm from opto.optimizers import OptoPrimeV2 -from opto.trainer.guide import AutoGuide +from opto.trainer.guide import Guide as _Guide from opto.utils.llm import DummyLLM import re, os @@ -40,7 +40,7 @@ def test_saving_load(): def test_trainer_saving_loading(): - class Guide(AutoGuide): + class Guide(_Guide): def get_feedback(self, query, response, reference=None, **kwargs): """ From dd51f6a99455868836654df096d30a04d413bd42 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 15 Aug 2025 21:46:19 +0000 Subject: [PATCH 156/172] Rename AlgorithmBase to Trainer --- opto/features/priority_search/utils.py | 2 +- opto/trainer/algorithms/UCBsearch.py | 6 +++--- opto/trainer/algorithms/algorithm.py | 2 +- opto/trainer/algorithms/basic_algorithms.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/opto/features/priority_search/utils.py b/opto/features/priority_search/utils.py index c12e3ded..e4c6906d 100644 --- a/opto/features/priority_search/utils.py +++ b/opto/features/priority_search/utils.py @@ -7,7 +7,7 @@ from opto.trace.nodes import ParameterNode from opto.trainer.utils import async_run, batch_run from opto.optimizers.utils import print_color -from opto.trainer.algorithms.basic_algorithms import Minibatch, AlgorithmBase, batchify +from opto.trainer.algorithms.basic_algorithms import Minibatch, Trainer, batchify from opto.trainer.loader import DataLoader from opto.features.priority_search.sampler import Sampler, RolloutsGraph import time diff --git a/opto/trainer/algorithms/UCBsearch.py b/opto/trainer/algorithms/UCBsearch.py index 71387ace..21bc9455 100644 --- a/opto/trainer/algorithms/UCBsearch.py +++ b/opto/trainer/algorithms/UCBsearch.py @@ -100,8 +100,8 @@ def _evaluate_candidate(self, self.optimizer.update(original_params) avg_score = np.mean(eval_scores) if ((eval_scores is not None) and all(s is not None for s in eval_scores)) else -np.inf - eval_count = len(eval_xs) - + eval_count = len(eval_xs) + return float(avg_score), eval_count def _calculate_ucb(self, candidate_buffer_entry: Dict, total_tracked_evaluations: int) -> float: @@ -337,7 +337,7 @@ def train(self, if save_frequency is not None and iteration % save_frequency == 0: best_overall_candidate = max(self.buffer, key=lambda c: c['score_sum'] / (c['eval_count'] or 1E-9) ) self.optimizer.update(best_overall_candidate['params']) # Load params using optimizer - self.save_agent(save_path, iteration) # save_agent is from AlgorithmBase + self.save_agent(save_path, iteration) # save_agent is from Trainer print_color(f"Iter {iteration}: Saved agent based on best candidate in buffer.", 'green') # End of search loop diff --git a/opto/trainer/algorithms/algorithm.py b/opto/trainer/algorithms/algorithm.py index 25180ef9..326e1be2 100644 --- a/opto/trainer/algorithms/algorithm.py +++ b/opto/trainer/algorithms/algorithm.py @@ -18,7 +18,7 @@ def train(self, *args, **kwargs): pass -class AlgorithmBase(AbstractAlgorithm): +class Trainer(AbstractAlgorithm): """ We define the API of algorithms to train an agent from a dataset of (x, info) pairs. diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index 194bb1c9..e9840851 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -2,7 +2,7 @@ import copy from typing import Union from opto import trace -from opto.trainer.algorithms.algorithm import AlgorithmBase +from opto.trainer.algorithms.algorithm import Trainer from opto.trainer.loader import DataLoader from opto.trainer.utils import batch_run, async_run from opto.optimizers.utils import print_color @@ -33,7 +33,7 @@ def standard_optimization_step(agent, x, guide, info, min_score=0): return target, score, feedback -class Minibatch(AlgorithmBase): +class Minibatch(Trainer): """ General minibatch optimization algorithm. This class defines a general training and logging routine using minimbatch sampling.""" def __init__(self, From dcd810187286cab5f3bbfa8b90a5b94f3f1bf06a Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 15 Aug 2025 22:48:32 +0000 Subject: [PATCH 157/172] Update version number to v0.2.0 --- opto/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/version.py b/opto/version.py index a2d08535..d3ec452c 100644 --- a/opto/version.py +++ b/opto/version.py @@ -1 +1 @@ -__version__ = "0.1.3.9" +__version__ = "0.2.0" From b2422957c812b2c30ba1c72c058997f4639c9142 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 19 Aug 2025 18:31:06 -0400 Subject: [PATCH 158/172] Fix mkdocstrings cross-reference warnings in LLM docstring - Update docstring format to avoid mkdocstrings parsing issues - Resolves 'Could not find cross-reference target' warnings in strict mode --- opto/utils/llm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opto/utils/llm.py b/opto/utils/llm.py index 320ba2b2..a53abbc7 100644 --- a/opto/utils/llm.py +++ b/opto/utils/llm.py @@ -32,7 +32,7 @@ def __init__(self, factory: Callable, reset_freq: Union[int, None] = None) -> No # Overwrite this `model` property when subclassing. @property def model(self): - """ When self.model is called, text responses should always be available at ['choices'][0].['message']['content'] """ + """When self.model is called, text responses should always be available at `response['choices'][0]['message']['content']`""" return self._model # This is the main API From e9bfb0731a6a8dca0fdae24e29a1a9a9f40ce7c6 Mon Sep 17 00:00:00 2001 From: chinganc Date: Tue, 19 Aug 2025 23:54:37 +0000 Subject: [PATCH 159/172] Fix missing oprov2 problem --- opto/optimizers/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opto/optimizers/__init__.py b/opto/optimizers/__init__.py index 9b0b2007..482b1b2d 100644 --- a/opto/optimizers/__init__.py +++ b/opto/optimizers/__init__.py @@ -1,9 +1,10 @@ from opto.optimizers.optoprime import OptoPrime as OptoPrimeV1 from opto.optimizers.optoprimemulti import OptoPrimeMulti from opto.optimizers.opro import OPRO +from opto.optimizers.opro_v2 import OPROv2 from opto.optimizers.textgrad import TextGrad from opto.optimizers.optoprime_v2 import OptoPrimeV2 OptoPrime = OptoPrimeV1 -__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2", "OptoPrimeV1"] \ No newline at end of file +__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad", "OptoPrimeV2", "OptoPrimeV1", "OPROv2"] \ No newline at end of file From e258e879f072c3bcf0cf626334270c66a2708bf1 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 20 Aug 2025 00:27:39 +0000 Subject: [PATCH 160/172] Add an assertion to make optimizer receives non-empty parameters. --- opto/optimizers/optimizer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opto/optimizers/optimizer.py b/opto/optimizers/optimizer.py index 04f8ea5e..2b175d5f 100644 --- a/opto/optimizers/optimizer.py +++ b/opto/optimizers/optimizer.py @@ -12,6 +12,7 @@ class AbstractOptimizer: def __init__(self, parameters: List[ParameterNode], *args, **kwargs): assert type(parameters) is list assert all([isinstance(p, ParameterNode) for p in parameters]) + assert len(parameters) > 0, 'Parameters list is empty.' self.parameters = parameters def step(self): From d2d744c04184e1d622e0f3f8761efdd2911f597d Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 20 Aug 2025 17:09:01 -0400 Subject: [PATCH 161/172] quick comment cleanup of opro_v2 --- opto/optimizers/opro_v2.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/opto/optimizers/opro_v2.py b/opto/optimizers/opro_v2.py index 3b66c14a..13054943 100644 --- a/opto/optimizers/opro_v2.py +++ b/opto/optimizers/opro_v2.py @@ -5,14 +5,6 @@ from opto.optimizers.optoprime_v2 import OptoPrimeV2, OptimizerPromptSymbolSet -""" -OPRO is a single parameter / solution optimizer that conditions on feedback. -(context, solution, feedback) -> new_solution - -It does not contain execution graph and is more streamlined/faster in inference. -""" - - # Not inheriting from optoprime_v2 because this should have a smaller set class OPROPromptSymbolSet(OptimizerPromptSymbolSet): @@ -69,11 +61,6 @@ def __repr__(self) -> str: feedback=self.feedback, ) -""" -TODO: -1. think about how initial solution was generated... -""" - class OPROv2(OptoPrimeV2): representation_prompt = dedent( """ From 3d444cb79150fa2f412f17caef726c333ff02f44 Mon Sep 17 00:00:00 2001 From: chinganc Date: Thu, 21 Aug 2025 06:26:54 +0000 Subject: [PATCH 162/172] Add a prototype --- opto/trainer/algorithms/__init__.py | 1 + opto/trainer/train.py | 116 ++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 opto/trainer/train.py diff --git a/opto/trainer/algorithms/__init__.py b/opto/trainer/algorithms/__init__.py index 2586fd31..09333a7f 100644 --- a/opto/trainer/algorithms/__init__.py +++ b/opto/trainer/algorithms/__init__.py @@ -1,3 +1,4 @@ +from opto.trainer.algorithms.algorithm import Trainer from opto.trainer.algorithms.basic_algorithms import Minibatch, MinibatchAlgorithm, BasicSearchAlgorithm from opto.trainer.algorithms.beamsearch_algorithm import BeamsearchAlgorithm, BeamsearchHistoryAlgorithm from opto.trainer.algorithms.UCBsearch import UCBSearchAlgorithm diff --git a/opto/trainer/train.py b/opto/trainer/train.py new file mode 100644 index 00000000..e4dbf2fb --- /dev/null +++ b/opto/trainer/train.py @@ -0,0 +1,116 @@ +from typing import Union +import importlib + +from opto import trace +from opto.train.algorithms import Trainer +from opto.trainer.guide import Guide +from opto.trainer.loggers import BaseLogger +from opto.optimizers.optimizer import Optimzier + + + +def dataset_check(dataset): + assert isinstance(dataset, dict), "Dataset must be a dictionary" + assert 'inputs' in dataset and 'infos' in dataset, "Dataset must contain 'inputs' and 'infos' keys" + assert len(dataset['inputs'])==len(dataset['infos']), "Inputs and infos must have the same length" + + + +def train( + model: trace.Module, + guide: Guide, + train_dataset: dict, + # TODO update the acceptable type of optimizer, trainer, guide, logger to be union of base class and str + optimizer: Union[Optimizer, str] = "OptoPrimeV2", + trainer: Union[Trainer, str] = 'BasicSearchAlgorithm', + guide: Union[Guide, str] = 'LLMGuide', + logger: Union[BaseLogger, str] = 'ConsoleLogger', + # extra configs + optimizer_kwargs: Union[dict, None] = None, + trainer_kwargs: Union[dict, None] = None # for train function + # TODO other kwargs +) -> None: + + """ A high-level helper function to train the model using trainer. """ + optimizer_kwargs = optimizer_kwargs or {} # this can be used to pass extra optimizer configs, like llm object explictly + trainer_kwargs = trainer_kwargs or {} + + # TODO check eligible optimizer, trainer + dataset_check(train_dataset) + + # TODO remove duplicate codes + + # Load optimizer from opto.optimizers + parameters = agent.parameters() + assert len(parameters) >0, "Agent must have parameters." + if type(optimizer) is str: + # check if optimizer is a valid class + optimizers_module = importlib.import_module("opto.optimizers") + optimizer_class = getattr(optimizers_module, optimizer) + optimizer = optimizer_class( + model.parameters(), + **optimizer_kwargs + ) + # else if optimizer is an instance + elif issubclass(optimizer, Optimizer): + optimizer = optimizer( + model.parameters(), + **optimizer_kwargs + ) + else: + raise ValueError(f"Invalid optimizer type: {type(optimizer)}") + + # Load guide from opto.trainer.guide + if type(guide) is str: + # check if guide is a valid class + guides_module = importlib.import_module("opto.trainer.guide") + guide_class = getattr(guides_module, guide) + guide = guide_class( + **guide_kwargs + ) + # else if guide is an instance + elif issubclass(guide, Guide): + guide = guide( + **guide_kwargs + ) + else: + raise ValueError(f"Invalid guide type: {type(guide)}") + + # Load logger from opto.trainer.loggers + if type(logger) is str: + # check if logger is a valid class + loggers_module = importlib.import_module("opto.trainer.loggers") + logger_class = getattr(loggers_module, logger) + logger = logger_class(**logger_kwargs) + # else if logger is an instance + elif issubclass(logger, BaseLogger): + logger = logger( + **logger_kwargs + ) + else: + raise ValueError(f"Invalid logger type: {type(logger)}") + + + # Load trainer from opto.trainer.algorithms + if type(trainer) is str: + # check if trainer is a valid class + trainers_module = importlib.import_module("opto.trainer.algorithms") + trainer_class = getattr(trainers_module, trainer) + trainer = trainer_class( + agent, + optimizer, + logger + ) + # else if trainer is an instance + elif issubclass(trainer, Trainer): + trainer = trainer( + agent, + optimizer, + logger + ) + else: + raise ValueError(f"Invalid trainer type: {type(trainer)}") + + + # TODO start training + trainer.train(**trainer_kwargs) \ No newline at end of file From 50c7adca51dd7cf560d1c1e1f140b3f19a53bf14 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 22 Aug 2025 21:53:09 +0000 Subject: [PATCH 163/172] Update train.py --- opto/trainer/train.py | 79 +++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/opto/trainer/train.py b/opto/trainer/train.py index e4dbf2fb..92eefe63 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -2,93 +2,93 @@ import importlib from opto import trace -from opto.train.algorithms import Trainer +from opto.trainer.algorithms import Trainer from opto.trainer.guide import Guide from opto.trainer.loggers import BaseLogger from opto.optimizers.optimizer import Optimzier - def dataset_check(dataset): assert isinstance(dataset, dict), "Dataset must be a dictionary" assert 'inputs' in dataset and 'infos' in dataset, "Dataset must contain 'inputs' and 'infos' keys" assert len(dataset['inputs'])==len(dataset['infos']), "Inputs and infos must have the same length" - def train( model: trace.Module, guide: Guide, train_dataset: dict, - # TODO update the acceptable type of optimizer, trainer, guide, logger to be union of base class and str - optimizer: Union[Optimizer, str] = "OptoPrimeV2", + # class of optimizer trainer: Union[Trainer, str] = 'BasicSearchAlgorithm', + optimizer: Union[Optimizer, str] = "OptoPrimeV2", guide: Union[Guide, str] = 'LLMGuide', logger: Union[BaseLogger, str] = 'ConsoleLogger', # extra configs optimizer_kwargs: Union[dict, None] = None, - trainer_kwargs: Union[dict, None] = None # for train function - # TODO other kwargs + guide_kwargs: Union[dict, None] = None, + logger_kwargs: Union[dict, None] = None, + # The rest is treated as trainer config + **trainer_kwargs, ) -> None: + """ A high-level helper function to train the model using trainer. + + A trainer algorithm applies an optimizer to train a model under a guide on a train_dataset. - """ A high-level helper function to train the model using trainer. """ + """ optimizer_kwargs = optimizer_kwargs or {} # this can be used to pass extra optimizer configs, like llm object explictly - trainer_kwargs = trainer_kwargs or {} + guide_kwargs = guide_kwargs or {} + logger_kwargs = logger_kwargs or {} # TODO check eligible optimizer, trainer dataset_check(train_dataset) # TODO remove duplicate codes - # Load optimizer from opto.optimizers + # Check agent parameters is non-empty parameters = agent.parameters() assert len(parameters) >0, "Agent must have parameters." + + + # Load optimizer from opto.optimizers if type(optimizer) is str: # check if optimizer is a valid class optimizers_module = importlib.import_module("opto.optimizers") optimizer_class = getattr(optimizers_module, optimizer) - optimizer = optimizer_class( - model.parameters(), - **optimizer_kwargs - ) # else if optimizer is an instance elif issubclass(optimizer, Optimizer): - optimizer = optimizer( - model.parameters(), - **optimizer_kwargs - ) + optimizer_class = optimizer else: raise ValueError(f"Invalid optimizer type: {type(optimizer)}") + optimizer = optimizer_class( + model.parameters(), + **optimizer_kwargs + ) # Load guide from opto.trainer.guide if type(guide) is str: # check if guide is a valid class guides_module = importlib.import_module("opto.trainer.guide") guide_class = getattr(guides_module, guide) - guide = guide_class( - **guide_kwargs - ) # else if guide is an instance elif issubclass(guide, Guide): - guide = guide( - **guide_kwargs - ) + guide_class = guide else: raise ValueError(f"Invalid guide type: {type(guide)}") + guide = guide_class( + **guide_kwargs + ) # Load logger from opto.trainer.loggers if type(logger) is str: # check if logger is a valid class loggers_module = importlib.import_module("opto.trainer.loggers") logger_class = getattr(loggers_module, logger) - logger = logger_class(**logger_kwargs) # else if logger is an instance elif issubclass(logger, BaseLogger): - logger = logger( - **logger_kwargs - ) + logger_class = logger else: raise ValueError(f"Invalid logger type: {type(logger)}") + logger = logger_class(**logger_kwargs) # Load trainer from opto.trainer.algorithms @@ -96,21 +96,18 @@ def train( # check if trainer is a valid class trainers_module = importlib.import_module("opto.trainer.algorithms") trainer_class = getattr(trainers_module, trainer) - trainer = trainer_class( - agent, - optimizer, - logger - ) # else if trainer is an instance elif issubclass(trainer, Trainer): - trainer = trainer( - agent, - optimizer, - logger - ) + trainer_class = trainer else: raise ValueError(f"Invalid trainer type: {type(trainer)}") + trainer = trainer_class( + agent, + optimizer, + logger + ) - - # TODO start training - trainer.train(**trainer_kwargs) \ No newline at end of file + return trainer.train( + guide=guide, + train_dataset=train_dataset, + **trainer_kwargs) From 3c79cca06601f35b605a34714baea01ad8f5455e Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 22 Aug 2025 22:22:49 +0000 Subject: [PATCH 164/172] Make train runnable and add an example code --- examples/train_example.py | 81 +++++++++++++++++++++++++++++++++ opto/trainer/__init__.py | 1 + opto/trainer/guide.py | 2 +- opto/trainer/train.py | 94 +++++++++++++++++++++------------------ 4 files changed, 133 insertions(+), 45 deletions(-) create mode 100644 examples/train_example.py diff --git a/examples/train_example.py b/examples/train_example.py new file mode 100644 index 00000000..a01bc594 --- /dev/null +++ b/examples/train_example.py @@ -0,0 +1,81 @@ +import datasets +import numpy as np +from opto import trace, trainer +from opto.utils.llm import LLM, LiteLLM + +from typing import Any + + +def call_llm(llm, system_prompt: str, user_prompt_template: str, message: str) -> str: + if '{message}' not in user_prompt_template: + raise ValueError("user_prompt_template must contain '{message}'") + response = llm( + messages=[{"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt_template.format(message=message)}] + ) + return response.choices[0].message.content + + +@trace.model +class Learner: + """ A basic LLM agent. """ + + def __init__(self, system_prompt: str = "You're a helpful agent", + user_prompt_template: str = "Query: {message}", + llm: LLM = None): + self.system_prompt = trace.node(system_prompt, trainable=True) + self.user_prompt_template = trace.node(user_prompt_template) + self.llm = llm or LLM() + + @trace.bundle() + def model(self, system_prompt: str, user_prompt_template: str, message: str) -> str: + """Call the LLM model. + + Args: + system_prompt: the system prompt to the agent. By tuning this prompt, we can control the behavior of the agent. For example, it can be used to provide instructions to the agent (such as how to reason about the problem, how to answer the question), or provide in-context examples of how to solve the problem. + user_prompt_template: the user prompt template to the agent. It is used as formatting the input to the agent as user_prompt_template.format(message=message). + message: the input to the agent. It can be a query, a task, a code, etc. + Returns: + The response from the agent. + """ + return call_llm(self.llm, self.system_prompt, self.user_prompt_template, message) + + def forward(self, message: Any) -> Any: + """ Forward pass of the agent. """ + return self.model(self.system_prompt, self.user_prompt_template, message) + + + +def main(): + # set seed + seed = 42 + num_epochs = 1 + batch_size = 3 # number of queries to sample from the training data + eval_frequency = -1 + + num_threads = 10 + datasize = 5 + + np.random.seed(seed) + + # In this example, we use the GSM8K dataset, which is a dataset of math word problems. + # We will look the training error of the agent on a small portion of this dataset. + train_dataset = datasets.load_dataset('BBEH/bbeh')['train'][:datasize] + train_dataset = dict(inputs=train_dataset['input'], infos=train_dataset['target']) + + agent = Learner(llm=LLM()) + + trainer.train( + model=agent, + train_dataset=train_dataset, + # trainer kwargs + num_epochs=num_epochs, + batch_size=batch_size, + eval_frequency=eval_frequency, + num_threads=num_threads, + verbose='output', + ) + + +if __name__ == "__main__": + main() diff --git a/opto/trainer/__init__.py b/opto/trainer/__init__.py index e69de29b..de4d59a8 100644 --- a/opto/trainer/__init__.py +++ b/opto/trainer/__init__.py @@ -0,0 +1 @@ +from opto.trainer.train import train \ No newline at end of file diff --git a/opto/trainer/guide.py b/opto/trainer/guide.py index 53f225ca..72e4b918 100644 --- a/opto/trainer/guide.py +++ b/opto/trainer/guide.py @@ -95,7 +95,7 @@ def __init__(self, prompt_template: Optional[str] = None, system_prompt: Optional[str] = None, correctness_template: Optional[str] = None, - use_formatted_response: bool = True + use_formatted_response: bool = False ): """ Initialize the VerbalGuide with an LLM and prompt templates. diff --git a/opto/trainer/train.py b/opto/trainer/train.py index 92eefe63..7e603a12 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -5,7 +5,7 @@ from opto.trainer.algorithms import Trainer from opto.trainer.guide import Guide from opto.trainer.loggers import BaseLogger -from opto.optimizers.optimizer import Optimzier +from opto.optimizers.optimizer import Optimizer def dataset_check(dataset): @@ -15,13 +15,13 @@ def dataset_check(dataset): def train( + *, model: trace.Module, - guide: Guide, train_dataset: dict, # class of optimizer - trainer: Union[Trainer, str] = 'BasicSearchAlgorithm', + algorithm: Union[Trainer, str] = 'BasicSearchAlgorithm', optimizer: Union[Optimizer, str] = "OptoPrimeV2", - guide: Union[Guide, str] = 'LLMGuide', + guide: Union[Guide, str] = 'LLMJudge', logger: Union[BaseLogger, str] = 'ConsoleLogger', # extra configs optimizer_kwargs: Union[dict, None] = None, @@ -44,70 +44,76 @@ def train( # TODO remove duplicate codes - # Check agent parameters is non-empty - parameters = agent.parameters() - assert len(parameters) >0, "Agent must have parameters." + # Check model parameters is non-empty + parameters = model.parameters() + assert len(parameters) >0, "Model must have non-empty parameters." + optimizer = load_optimizer(optimizer, model, **optimizer_kwargs) + guide = load_guide(guide, **guide_kwargs) + logger = load_logger(logger, **logger_kwargs) + trainer_class = load_trainer_class(algorithm) - # Load optimizer from opto.optimizers - if type(optimizer) is str: - # check if optimizer is a valid class + assert isinstance(optimizer, Optimizer) + assert isinstance(guide, Guide) + assert isinstance(logger, BaseLogger) + assert issubclass(trainer_class, Trainer) + + algo = trainer_class( + model, + optimizer, + logger + ) + + return algo.train( + guide=guide, + train_dataset=train_dataset, + **trainer_kwargs) + + +def load_optimizer(optimizer: Union[Optimizer, str], model: trace.Module, **kwargs) -> Optimizer: + if isinstance(optimizer, Optimizer): + return optimizer + elif isinstance(optimizer, str): optimizers_module = importlib.import_module("opto.optimizers") optimizer_class = getattr(optimizers_module, optimizer) - # else if optimizer is an instance + return optimizer_class(model.parameters(), **kwargs) elif issubclass(optimizer, Optimizer): - optimizer_class = optimizer + return optimizer(model.parameters(), **kwargs) else: raise ValueError(f"Invalid optimizer type: {type(optimizer)}") - optimizer = optimizer_class( - model.parameters(), - **optimizer_kwargs - ) - # Load guide from opto.trainer.guide - if type(guide) is str: - # check if guide is a valid class + +def load_guide(guide: Union[Guide, str], **kwargs) -> Guide: + if isinstance(guide, Guide): + return guide + elif isinstance(guide, str): guides_module = importlib.import_module("opto.trainer.guide") guide_class = getattr(guides_module, guide) - # else if guide is an instance + return guide_class(**kwargs) elif issubclass(guide, Guide): - guide_class = guide + return guide(**kwargs) else: raise ValueError(f"Invalid guide type: {type(guide)}") - guide = guide_class( - **guide_kwargs - ) - # Load logger from opto.trainer.loggers - if type(logger) is str: - # check if logger is a valid class +def load_logger(logger: Union[BaseLogger, str], **kwargs) -> BaseLogger: + if isinstance(logger, BaseLogger): + return logger + elif isinstance(logger, str): loggers_module = importlib.import_module("opto.trainer.loggers") logger_class = getattr(loggers_module, logger) - # else if logger is an instance + return logger_class(**kwargs) elif issubclass(logger, BaseLogger): - logger_class = logger + return logger(**kwargs) else: raise ValueError(f"Invalid logger type: {type(logger)}") - logger = logger_class(**logger_kwargs) - - # Load trainer from opto.trainer.algorithms - if type(trainer) is str: - # check if trainer is a valid class +def load_trainer_class(trainer: Union[Trainer, str]) -> Trainer: + if isinstance(trainer, str): trainers_module = importlib.import_module("opto.trainer.algorithms") trainer_class = getattr(trainers_module, trainer) - # else if trainer is an instance elif issubclass(trainer, Trainer): trainer_class = trainer else: raise ValueError(f"Invalid trainer type: {type(trainer)}") - trainer = trainer_class( - agent, - optimizer, - logger - ) - return trainer.train( - guide=guide, - train_dataset=train_dataset, - **trainer_kwargs) + return trainer_class \ No newline at end of file From 6def8faba79342b43e0b649182d1825951a351d7 Mon Sep 17 00:00:00 2001 From: chinganc Date: Fri, 22 Aug 2025 22:29:10 +0000 Subject: [PATCH 165/172] Fix a bug in the example code. Set minibatch's ensure improvement to be true. --- examples/train_example.py | 2 +- opto/trainer/algorithms/basic_algorithms.py | 2 +- opto/trainer/train.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/train_example.py b/examples/train_example.py index a01bc594..10b76e0a 100644 --- a/examples/train_example.py +++ b/examples/train_example.py @@ -38,7 +38,7 @@ def model(self, system_prompt: str, user_prompt_template: str, message: str) -> Returns: The response from the agent. """ - return call_llm(self.llm, self.system_prompt, self.user_prompt_template, message) + return call_llm(self.llm, system_prompt, user_prompt_template, message) def forward(self, message: Any) -> Any: """ Forward pass of the agent. """ diff --git a/opto/trainer/algorithms/basic_algorithms.py b/opto/trainer/algorithms/basic_algorithms.py index e9840851..76597dcb 100644 --- a/opto/trainer/algorithms/basic_algorithms.py +++ b/opto/trainer/algorithms/basic_algorithms.py @@ -53,7 +53,7 @@ def train(self, guide, train_dataset, *, - ensure_improvement: bool = False, # whether to check the improvement of the agent + ensure_improvement: bool = True, # whether to check the improvement of the agent improvement_threshold: float = 0., # threshold for improvement num_epochs: int = 1, # number of training epochs batch_size: int = 1, # batch size for updating the agent diff --git a/opto/trainer/train.py b/opto/trainer/train.py index 7e603a12..e46d8c4d 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -19,7 +19,7 @@ def train( model: trace.Module, train_dataset: dict, # class of optimizer - algorithm: Union[Trainer, str] = 'BasicSearchAlgorithm', + algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', optimizer: Union[Optimizer, str] = "OptoPrimeV2", guide: Union[Guide, str] = 'LLMJudge', logger: Union[BaseLogger, str] = 'ConsoleLogger', From 4ab8cca80fac4ff9d4c91f2130e79688a914156f Mon Sep 17 00:00:00 2001 From: Adith Swaminathan Date: Mon, 25 Aug 2025 09:46:24 -0700 Subject: [PATCH 166/172] Clean up obsolete files --- OAI_CONFIG_LIST_sample | 25 - docs/_static/custom.css | 35 - docs/colab_kernel_clean_script.py | 29 - docs/jupyter_build.sh | 16 - docs/post_build_script.py | 48 - docs/publish.sh | 6 - docs/requirements.txt | 8 - generated_docs/opto/optimizers/buffers.md | 76 - .../opto/optimizers/function_optimizer.md | 738 ------ generated_docs/opto/optimizers/opro.md | 79 - generated_docs/opto/optimizers/optimizers.md | 267 -- generated_docs/opto/trace/broadcast.md | 54 - generated_docs/opto/trace/bundle.md | 469 ---- generated_docs/opto/trace/containers.md | 386 --- generated_docs/opto/trace/errors.md | 112 - generated_docs/opto/trace/modules.md | 304 --- generated_docs/opto/trace/nodes.md | 2213 ----------------- generated_docs/opto/trace/operators.md | 893 ------- .../trace/propagators/graph_propagator.md | 166 -- .../opto/trace/propagators/propagators.md | 338 --- generated_docs/opto/trace/trace.md | 43 - generated_docs/opto/trace/utils.md | 320 --- 22 files changed, 6625 deletions(-) delete mode 100644 OAI_CONFIG_LIST_sample delete mode 100644 docs/_static/custom.css delete mode 100644 docs/colab_kernel_clean_script.py delete mode 100644 docs/jupyter_build.sh delete mode 100644 docs/post_build_script.py delete mode 100644 docs/publish.sh delete mode 100644 docs/requirements.txt delete mode 100644 generated_docs/opto/optimizers/buffers.md delete mode 100644 generated_docs/opto/optimizers/function_optimizer.md delete mode 100644 generated_docs/opto/optimizers/opro.md delete mode 100644 generated_docs/opto/optimizers/optimizers.md delete mode 100644 generated_docs/opto/trace/broadcast.md delete mode 100644 generated_docs/opto/trace/bundle.md delete mode 100644 generated_docs/opto/trace/containers.md delete mode 100644 generated_docs/opto/trace/errors.md delete mode 100644 generated_docs/opto/trace/modules.md delete mode 100644 generated_docs/opto/trace/nodes.md delete mode 100644 generated_docs/opto/trace/operators.md delete mode 100644 generated_docs/opto/trace/propagators/graph_propagator.md delete mode 100644 generated_docs/opto/trace/propagators/propagators.md delete mode 100644 generated_docs/opto/trace/trace.md delete mode 100644 generated_docs/opto/trace/utils.md diff --git a/OAI_CONFIG_LIST_sample b/OAI_CONFIG_LIST_sample deleted file mode 100644 index 74f87d30..00000000 --- a/OAI_CONFIG_LIST_sample +++ /dev/null @@ -1,25 +0,0 @@ -// Please modify the content, remove these four lines of comment and rename this file to OAI_CONFIG_LIST to run the sample code. -// If using pyautogen v0.1.x with Azure OpenAI, please replace "base_url" with "api_base" (line 14 and line 21 below). Use "pip list" to check version of pyautogen installed. -// -// NOTE: This configuration lists GPT-4 as the default model, as this represents our current recommendation, and is known to work well with AutoGen. If you use a model other than GPT-4, you may need to revise various system prompts (especially if using weaker models like GPT-3.5-turbo). Moreover, if you use models other than those hosted by OpenAI or Azure, you may incur additional risks related to alignment and safety. Proceed with caution if updating this default. -[ - { - "model": "gpt-4", - "api_key": "", - "tags": ["gpt-4", "tool"] - }, - { - "model": "", - "api_key": "", - "base_url": "", - "api_type": "azure", - "api_version": "" - }, - { - "model": "", - "api_key": "", - "base_url": "", - "api_type": "azure", - "api_version": "" - } -] \ No newline at end of file diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index cd2f03fd..00000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,35 +0,0 @@ -:root { - --sd-color-primary: #f37726; - --sd-color-primary-highlight: #da864e; - --sd-color-secondary: #267bf3; - --sd-color-secondary-highlight: #4e88da; -} - -.bg-jb-one { - background-color: #52d16f3b; -} - -.bg-jb-two { - background-color: #e7dd7b73; -} - -.bg-jb-three { - background-color: #e7b07b96; -} - -.admonition>.admonition-title, div.admonition>.admonition-title { - background-color: #eef9fd; -} - -.admonition, div.admonition { - background-color: white; - border-color: #4cb3d4; -} - -.admonition p { - color: #474747; -} - -.text_html p { - color: #474747; -} \ No newline at end of file diff --git a/docs/colab_kernel_clean_script.py b/docs/colab_kernel_clean_script.py deleted file mode 100644 index 90d9ff3d..00000000 --- a/docs/colab_kernel_clean_script.py +++ /dev/null @@ -1,29 +0,0 @@ -import shutil -import os -import json - -# Figure out if we are in the `docs` directory or the root directory -if os.path.exists('index.html'): - print("Found index.html in current directory, assuming we are in the root directory") -else: - print("In the root directory, changing to docs directory") - os.chdir('docs') - if not os.path.exists('_config.yml'): - raise FileNotFoundError("Could not find _config.yml in the root directory or the docs directory.") - -# Clean up Jupyter notebooks (remove kernel-spec) -for root, dirs, files in os.walk('.'): - for file in files: - if file.endswith('.ipynb'): - print(root, file) - with open(os.path.join(root, file), 'r') as f: - try: - data = json.load(f) - except json.JSONDecodeError: - print("Could not read JSON, skipping", file) - continue - if 'kernelspec' in data['metadata']: - print("removed kernel", data['metadata']['kernelspec']) - del data['metadata']['kernelspec'] - with open(os.path.join(root, file), 'w') as f: - json.dump(data, f, indent=4) \ No newline at end of file diff --git a/docs/jupyter_build.sh b/docs/jupyter_build.sh deleted file mode 100644 index dd9ebf48..00000000 --- a/docs/jupyter_build.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -cd "$(dirname "$0")/.." || exit -rm -r docs/_build docs/api -ORIGINAL_PYTHONPATH=$PYTHONPATH -export PYTHONPATH=$(pwd)/..:$PYTHONPATH - -jupyter-book build docs - -# clean up sphinx-autosummary generated files -rm -r docs/api - -# Restored PYTHONPATH -export PYTHONPATH=$ORIGINAL_PYTHONPATH - -# move all files associated with the landing page into the `_build/html` folder -python docs/post_build_script.py \ No newline at end of file diff --git a/docs/post_build_script.py b/docs/post_build_script.py deleted file mode 100644 index 96f96679..00000000 --- a/docs/post_build_script.py +++ /dev/null @@ -1,48 +0,0 @@ -import shutil -import os -import json - -# Figure out if we are in the `docs` directory or the root directory -if os.path.exists('index.html'): - print("Found index.html in current directory, assuming we are in the root directory") -else: - print("In the root directory, changing to docs directory") - os.chdir('docs') - if not os.path.exists('index.html'): - raise FileNotFoundError("Could not find index.html in the root directory or the docs directory. Are you in the `website` branch?") - -# Path to your custom index.html -custom_index = 'index.html' -# Path to your images folder -images_folder = 'images' -# Path to the built book (adjust as needed) -built_book = '_build/html' -# Path to the images destination in the built book -built_images = os.path.join(built_book, 'images') - -# Copy the custom index.html to the built book directory -shutil.copy2(custom_index, os.path.join(built_book, 'index.html')) -print(f"Copied custom index.html to {built_book}") - - -def rm_and_copy(src, dst): - if os.path.exists(dst): - # If the directory exists, remove it first to ensure a clean copy - shutil.rmtree(dst) - # Copy the entire directory - shutil.copytree(src, dst) - print(f"Copied {src} to {dst}") - -# Copy the entire images directory -rm_and_copy('images', built_images) - -# Copy the vendor directory -rm_and_copy('vendor', os.path.join(built_book, 'vendor')) - -# Copy the css directory -rm_and_copy('css', os.path.join(built_book, 'css')) - -# Copy the assets directory -rm_and_copy('assets', os.path.join(built_book, 'assets')) - -print("Post-build process completed successfully!") \ No newline at end of file diff --git a/docs/publish.sh b/docs/publish.sh deleted file mode 100644 index b45f475a..00000000 --- a/docs/publish.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -cd "$(dirname "$0")/.." || exit -rm -r docs/_build -jupyter-book build docs -python docs/post_build_script.py -ghp-import -n -p -f docs/_build/html \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index e5ad1997..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -jupyter-book -matplotlib -numpy -sphinx -sphinx-plausible -# sphinx-autodoc2 -sphinx-autoapi -ghp-import \ No newline at end of file diff --git a/generated_docs/opto/optimizers/buffers.md b/generated_docs/opto/optimizers/buffers.md deleted file mode 100644 index e5c4c3a2..00000000 --- a/generated_docs/opto/optimizers/buffers.md +++ /dev/null @@ -1,76 +0,0 @@ -## ClassDef FIFOBuffer -**FIFOBuffer**: The function of FIFOBuffer is to manage a First-In-First-Out (FIFO) buffer of a specified size. - -**attributes**: The attributes of this Class. -· size: The maximum number of items the buffer can hold. -· buffer: A list that stores the items in the buffer. - -**Code Description**: The FIFOBuffer class is designed to handle a buffer that operates on a First-In-First-Out (FIFO) basis. This means that the first item added to the buffer will be the first one to be removed when the buffer reaches its maximum size. - -- The `__init__` method initializes the buffer with a specified size and creates an empty list to store the items. -- The `add` method allows adding an item to the buffer. If the buffer exceeds the specified size, it removes the oldest items to maintain the size constraint. -- The `__iter__` method returns an iterator for the buffer, allowing it to be used in loops and other iterable contexts. -- The `__len__` method returns the current number of items in the buffer. - -In the project, the FIFOBuffer is utilized by the `FunctionOptimizerV2Memory` class in the `opto\optimizers\function_optimizer.py` file. Specifically, it is instantiated in the `__init__` method of `FunctionOptimizerV2Memory` with a parameter `memory_size`, which determines the size of the FIFO buffer. This integration suggests that the FIFOBuffer is used to store a limited history of optimization states or results, ensuring that only the most recent entries are kept. - -**Note**: -- Ensure that the size parameter is a positive integer to avoid unexpected behavior. -- The buffer will automatically discard the oldest items when new items are added beyond its capacity. - -**Output Example**: -If a FIFOBuffer is created with a size of 3 and the following items are added in sequence: `1, 2, 3, 4`, the buffer will contain `[2, 3, 4]`. The first item `1` is discarded to maintain the buffer size of 3. -### FunctionDef __init__(self, size) -**__init__**: The function of __init__ is to initialize a FIFOBuffer object with a specified size. - -**parameters**: The parameters of this Function. -· size: An integer representing the maximum size of the buffer. - -**Code Description**: The __init__ function is a constructor method for the FIFOBuffer class. It takes one parameter, size, which determines the maximum number of elements that the buffer can hold. Inside the function, the size parameter is assigned to the instance variable self.size. Additionally, an empty list is initialized and assigned to the instance variable self.buffer. This list will be used to store the elements of the buffer. - -**Note**: -- The size parameter must be a positive integer. -- The buffer is initially empty upon creation of the FIFOBuffer object. -*** -### FunctionDef add(self, item) -**add**: The function of add is to insert a new item into the buffer while maintaining its maximum size. - -**parameters**: The parameters of this Function. -· item: The item to be added to the buffer. - -**Code Description**: The add function is a method of the FIFOBuffer class, which is designed to manage a buffer with a fixed maximum size. When a new item is added to the buffer, the function first checks if the buffer size is greater than zero. If it is, the item is appended to the buffer. After appending the item, the buffer is truncated to ensure that its size does not exceed the predefined maximum size. This is achieved by slicing the buffer to keep only the most recent items up to the specified size. - -In the context of its usage within the project, the add function is called by the construct_prompt method of the FunctionOptimizerV2Memory class. Specifically, after constructing the system and user prompts, the add function is used to store a tuple containing the summary variables and user feedback into the memory buffer. This ensures that the memory buffer maintains a record of past interactions, which can be used to provide examples in future prompts. - -**Note**: -- The buffer size must be set to a positive integer for the add function to operate correctly. -- The function ensures that the buffer does not grow beyond its maximum size, maintaining only the most recent items. -- Proper handling of the buffer size is crucial to avoid unexpected behavior. -*** -### FunctionDef __iter__(self) -**__iter__**: The function of __iter__ is to return an iterator for the buffer attribute of the FIFOBuffer instance. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The __iter__ function is a special method in Python that allows an object to be iterable. In this implementation, the __iter__ method returns an iterator for the buffer attribute of the FIFOBuffer instance. The buffer attribute is expected to be a collection (such as a list) that supports iteration. By calling iter(self.buffer), the method leverages Python's built-in iter function to obtain an iterator for the buffer, enabling the FIFOBuffer instance to be used in contexts that require iteration, such as in for-loops. - -**Note**: -- Ensure that the buffer attribute is properly initialized and contains iterable elements before invoking the __iter__ method. -- This method does not modify the buffer; it only provides a way to iterate over its elements. - -**Output Example**: -If the buffer attribute of the FIFOBuffer instance contains the elements [1, 2, 3], calling the __iter__ method will return an iterator that produces the sequence 1, 2, 3 when iterated over. -*** -### FunctionDef __len__(self) -**__len__**: The function of __len__ is to return the number of elements currently stored in the buffer. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The __len__ function is a special method in Python that is used to define the behavior of the len() function for instances of a class. In this context, the __len__ function returns the length of the buffer attribute of the FIFOBuffer class. The buffer attribute is expected to be a list or another collection that supports the len() function. When len() is called on an instance of FIFOBuffer, it internally calls this __len__ method, which in turn returns the length of the buffer. - -**Note**: Ensure that the buffer attribute is properly initialized and maintained as a collection that supports the len() function. If the buffer is not initialized or is set to a non-collection type, calling len() on an instance of FIFOBuffer will result in an error. - -**Output Example**: If the buffer contains 5 elements, calling len() on an instance of FIFOBuffer will return 5. -*** diff --git a/generated_docs/opto/optimizers/function_optimizer.md b/generated_docs/opto/optimizers/function_optimizer.md deleted file mode 100644 index 8d97f69f..00000000 --- a/generated_docs/opto/optimizers/function_optimizer.md +++ /dev/null @@ -1,738 +0,0 @@ -## FunctionDef get_fun_name(node) -**get_fun_name**: The function of get_fun_name is to retrieve the name of a MessageNode object. - -**parameters**: -- node: A MessageNode object. - -**Code Description**: -The `get_fun_name` function is used to retrieve the name of a `MessageNode` object. It takes a `node` parameter, which is an instance of the `MessageNode` class. - -The function first checks if the `info` attribute of the `node` object is a dictionary and if it contains the key "fun_name". If this condition is true, the function returns the value associated with that key. - -If the condition is false, the function splits the `name` attribute of the `node` object using the ":" delimiter. It then returns the first part of the split. - -The purpose of this function is to provide a convenient way to retrieve the name of a `MessageNode` object. The name can be used for various purposes, such as identifying the node in a graph or generating function calls. - -This function is called by the `repr_function_call` function in the `function_optimizer.py` file of the `optimizers` module. It is used to retrieve the name of a `MessageNode` object and include it in a function call representation. - -**Note**: -- The `get_fun_name` function assumes that the `node` object is an instance of the `MessageNode` class. -- The function relies on the `info` and `name` attributes of the `node` object to retrieve the name. - -**Output Example**: -If the `info` attribute of the `node` object is a dictionary with the key "fun_name" and the associated value is "my_function", calling `get_fun_name(node)` will return "my_function". - -## FunctionDef repr_function_call(child) -**repr_function_call**: The function of repr_function_call is to generate a string representation of a function call based on a MessageNode object. - -**parameters**: -- child: A MessageNode object. - -**Code Description**: -The `repr_function_call` function takes a `child` parameter, which is an instance of the `MessageNode` class. It generates a string representation of a function call based on the attributes of the `child` object. - -The function first initializes the `function_call` variable with the format "{child.py_name} = {get_fun_name(child)}(". This sets the initial part of the function call string, which includes the name of the variable assigned to the function call and the name of the function itself. - -Next, the function iterates over the `inputs` attribute of the `child` object, which is a dictionary containing the input nodes of the `MessageNode` object. For each key-value pair in the dictionary, the function appends "{k}={v.py_name}, " to the `function_call` string. This adds the input variable names and their corresponding values to the function call string. - -After the loop, the function removes the trailing ", " from the `function_call` string and adds a closing parenthesis. This completes the function call string. - -Finally, the function returns the `function_call` string. - -The purpose of this function is to provide a convenient way to generate a string representation of a function call based on a `MessageNode` object. The function call string can be used for various purposes, such as logging, debugging, or generating code. - -This function is called by the `node_to_function_feedback` function in the `function_optimizer.py` file of the `optimizers` module. It is used to generate the function call representation of a `MessageNode` object and include it in the `graph` list of the `FunctionFeedback` object. - -**Note**: -- The `repr_function_call` function assumes that the `child` object is an instance of the `MessageNode` class. -- The function relies on the `py_name` attribute of the input nodes to retrieve their variable names. -- The function relies on the `get_fun_name` function to retrieve the name of the `child` object. - -**Output Example**: -If the `child` object has the following attributes: -- `py_name`: "result" -- `inputs`: {"x": , "y": } - -Calling `repr_function_call(child)` will return the following string: -"result = my_function(x=node_x, y=node_y)" -## FunctionDef node_to_function_feedback(node_feedback) -**node_to_function_feedback**: The function of node_to_function_feedback is to convert a TraceGraph object into a FunctionFeedback object. It processes the nodes in the TraceGraph, categorizes them into roots, intermediates, and outputs, and populates the corresponding attributes of the FunctionFeedback object. - -**parameters**: -- node_feedback: A TraceGraph object representing the subgraph of nodes. - -**Code Description**: -The `node_to_function_feedback` function takes a `node_feedback` parameter, which is an instance of the `TraceGraph` class. It converts the `TraceGraph` object into a `FunctionFeedback` object by processing the nodes in the graph and organizing them into different categories. - -The function first initializes the `depth` variable based on the length of the `graph` attribute of the `node_feedback` object. If the `graph` attribute is empty, the depth is set to 0; otherwise, it is set to the last element's depth in the `graph` attribute. - -Next, the function initializes empty lists and dictionaries for `graph`, `others`, `roots`, `output`, and `documentation`. These variables will store the processed data and information. - -The function then creates a `visited` set to keep track of visited nodes. It iterates over the `graph` attribute of the `node_feedback` object, which contains tuples representing the level and node of the graph. For each level and node, it checks if the node is a root node by checking the `is_root` attribute. If it is a root node, it updates the `roots` dictionary with the node's name as the key and its data and constraint as the value. - -If the node is not a root node, it checks if all of its parents have been visited. If they have, it categorizes the node as an intermediate node. It updates the `documentation` dictionary with the node's name as the key and its description as the value. It appends a tuple representing the level and a string representation of the function call to the `graph` list. If the level is equal to the depth, it updates the `output` dictionary with the node's name as the key and its data and constraint as the value. Otherwise, it updates the `others` dictionary with the node's name as the key and its data and constraint as the value. - -If the node is not an intermediate node, it categorizes it as a blanket node and adds it to the `roots` dictionary. - -Finally, the function returns a `FunctionFeedback` object with the populated `graph`, `others`, `roots`, `output`, `user_feedback`, and `documentation` attributes. - -**Note**: -- The `node_to_function_feedback` function assumes that the `node_feedback` parameter is a valid instance of the `TraceGraph` class. -- The function relies on the attributes and methods of the `TraceGraph` class to process the nodes and extract the necessary information. -- The resulting `FunctionFeedback` object represents the converted feedback from the `TraceGraph` object. - -**Output Example**: -A possible return value of the `node_to_function_feedback` function could be a `FunctionFeedback` object with the following attributes: -- `graph`: [(0, "function_call_1"), (1, "function_call_2"), ...] -- `others`: {"node_name_1": (data_1, constraint_1), "node_name_2": (data_2, constraint_2), ...} -- `roots`: {"root_name_1": (data_1, constraint_1), "root_name_2": (data_2, constraint_2), ...} -- `output`: {"output_name_1": (data_1, constraint_1), "output_name_2": (data_2, constraint_2), ...} -- `user_feedback`: "User feedback string" -- `documentation`: {"node_name_1": "Node description 1", "node_name_2": "Node description 2", ...} -## ClassDef FunctionFeedback -**FunctionFeedback**: The function of FunctionFeedback is to serve as a feedback container used by the FunctionPropagator. - -**attributes**: The attributes of this Class. -· graph: Each item is a representation of a function call. The items are topologically sorted. -· documentation: Function name and its documentation string. -· others: Intermediate variable names and their data. -· roots: Root variable name and its data. -· output: Leaf variable name and its data. -· user_feedback: User feedback at the leaf of the graph. - -**Code Description**: The FunctionFeedback class is designed to encapsulate feedback information used by the FunctionPropagator. It organizes and stores various types of data related to function calls and their execution within a graph structure. The attributes of this class are as follows: - -- `graph`: This attribute holds a list of tuples, where each tuple represents a function call. The tuples are topologically sorted, ensuring that the order of function calls respects their dependencies. -- `documentation`: This dictionary maps function names to their corresponding documentation strings, providing a reference for understanding the purpose and behavior of each function. -- `others`: This dictionary stores intermediate variable names along with their associated data. These variables are neither root nor leaf nodes in the function call graph. -- `roots`: This dictionary contains root variable names and their data. Root variables are the starting points in the function call graph. -- `output`: This dictionary holds leaf variable names and their data. Leaf variables are the endpoints in the function call graph. -- `user_feedback`: This string captures user feedback at the leaf of the graph, providing insights or comments from the user regarding the final output. - -The FunctionFeedback class is utilized by the `node_to_function_feedback` function, which converts a TraceGraph into a FunctionFeedback instance. This conversion involves processing the nodes of the TraceGraph, categorizing them into roots, intermediates (others), and outputs, and then populating the corresponding attributes of the FunctionFeedback instance. The `node_to_function_feedback` function ensures that the graph is correctly sorted and that all relevant data and documentation are accurately captured. - -**Note**: Points to note about the use of the code -- Ensure that the input TraceGraph to the `node_to_function_feedback` function is correctly structured and sorted. -- The FunctionFeedback class relies on the accurate categorization of nodes into roots, intermediates, and outputs for proper functionality. -- User feedback should be meaningful and relevant to the final output to provide valuable insights. -## ClassDef ProblemInstance -**ProblemInstance**: The function of ProblemInstance is to encapsulate and format the details of a problem instance for optimization tasks. - -**attributes**: The attributes of this Class. -· instruction: A string containing the instructions for the problem. -· code: A string representing the code to be executed. -· documentation: A string providing documentation for the code. -· variables: A string listing the variables involved in the problem. -· inputs: A string detailing the inputs required for the code. -· others: A string for any additional information related to the problem. -· outputs: A string specifying the expected outputs of the code. -· feedback: A string containing feedback on the problem instance. -· constraints: A string outlining any constraints on the variables or the problem. - -**Code Description**: The ProblemInstance class is designed to encapsulate various components of a problem instance, such as instructions, code, documentation, variables, inputs, outputs, feedback, and constraints. It uses a predefined template to format these components into a structured string representation. - -The class includes a `problem_template` attribute, which is a formatted string template that organizes the problem details into sections. The `__repr__` method is overridden to return a formatted string representation of the problem instance using this template. - -The ProblemInstance class is utilized in the FunctionOptimizer class, specifically in its `__init__` and `probelm_instance` methods. In the `__init__` method, an example problem instance is created using the ProblemInstance class to demonstrate the expected format and structure. The `probelm_instance` method generates a new ProblemInstance based on the provided summary and an optional mask to exclude certain sections. - -**Note**: When using the ProblemInstance class, ensure that all attributes are properly populated to generate a meaningful and complete problem instance. The class relies on the provided template to format the output, so any missing or incorrect information may result in an incomplete or inaccurate representation. - -**Output Example**: -``` -#Instruction -Optimize the function to achieve the desired output. - -#Code -y = add(x=a,y=b) -z = subtract(x=y, y=c) - -#Documentation -add: add x and y -subtract: subtract y from x - -#Variables -(int) a = 5 - -#Constraints -a: a > 0 - -#Inputs -(int) b = 1 -(int) c = 5 - -#Others -(int) y = 6 - -#Outputs -(int) z = 1 - -#Feedback: -The result of the code is not as expected. The result should be 10, but the code returns 1 -``` -### FunctionDef __repr__(self) -**__repr__**: The function of __repr__ is to provide a formatted string representation of the ProblemInstance object. - -**parameters**: The parameters of this function. -· self: Refers to the instance of the ProblemInstance class. - -**Code Description**: The __repr__ function returns a string that represents the ProblemInstance object in a human-readable format. It uses the problem_template attribute of the instance to format the string. The placeholders in the problem_template are filled with the corresponding attributes of the instance, which include: -- instruction: Instructions related to the problem instance. -- code: The code associated with the problem instance. -- documentation: Documentation details of the problem instance. -- variables: Variables involved in the problem instance. -- constraints: Constraints applied to the problem instance. -- inputs: Inputs required for the problem instance. -- outputs: Outputs expected from the problem instance. -- others: Any other relevant information about the problem instance. -- feedback: Feedback related to the problem instance. - -**Note**: Ensure that the problem_template attribute is properly defined and contains the necessary placeholders for all the attributes used in the format method. If any attribute is missing or the template is incorrectly formatted, it may result in a runtime error. - -**Output Example**: A possible appearance of the code's return value could be: -``` -ProblemInstance( - instruction='Optimize the function', - code='def optimize(): pass', - documentation='This function optimizes the given parameters.', - variables={'x': 10, 'y': 20}, - constraints='x + y <= 30', - inputs=['x', 'y'], - outputs=['result'], - others='Additional information', - feedback='No issues found' -) -``` -*** -## ClassDef FunctionOptimizer -**FunctionOptimizer**: The function of FunctionOptimizer is to serve as a base class for optimizers, responsible for updating parameters based on feedback. - -**attributes**: -- parameters: A list of ParameterNode objects that the optimizer will manage and update. - -**Code Description**: -The FunctionOptimizer class is a subclass of the Optimizer class and provides a base implementation for optimizing functions. It extends the Optimizer class and overrides some of its methods to customize the optimization process. - -The `__init__` method initializes the FunctionOptimizer object by calling the superclass's `__init__` method and passing the parameters list. It also sets the `representation_prompt` attribute, which is a generic representation prompt explaining how to read and understand the problem. - -The `default_objective` attribute defines the default objective of the optimizer, which is to change the values of the variables in the `#Variables` section to improve the output according to the feedback. - -The `output_format_prompt` attribute defines the output format of the optimizer's response. It specifies that the output should be in JSON format and provides a template for the structure of the response. - -The `example_problem_template` attribute defines a template for an example problem instance and response. It includes placeholders for the problem instance and the response, which can be filled in with actual values. - -The `user_prompt_template` attribute defines a template for the user prompt. It includes placeholders for the problem instance and the instruction, which can be filled in with actual values. - -The `example_prompt` attribute is currently empty and marked as a TODO. It is intended to provide feasible but not optimal solutions for the current problem instance as a hint to help users understand the problem better. - -The `final_prompt` attribute defines a template for the final prompt, which prompts the user to provide their response. - -The `__init__` method also initializes other attributes such as `propagator`, `llm`, `ignore_extraction_error`, `include_example`, `max_tokens`, and `log` with default values or values passed as arguments. - -The `default_propagator` method returns the default Propagator object of the optimizer. This method is implemented in the Optimizer class and must be overridden by subclasses. - -The `summarize` method aggregates the feedback from all the parameters and constructs the summary object. It then classifies the root nodes into variables and others. - -The `repr_node_value` method takes a dictionary of node values and returns a string representation of the values. - -The `repr_node_constraint` method takes a dictionary of node constraints and returns a string representation of the constraints. - -The `probelm_instance` method constructs a ProblemInstance object based on the summary and a mask. The mask is used to exclude certain sections from the problem instance. - -The `construct_prompt` method constructs the system and user prompts based on the summary and a mask. The system prompt includes the representation prompt and the output format prompt. The user prompt includes the problem instance and the final prompt. - -The `_step` method is an abstract method that must be implemented by subclasses. It is responsible for proposing new parameter values based on feedback and returning the update dictionary. - -The `construct_update_dict` method converts the suggestion in text format into the right data type and constructs an update dictionary. - -The `extract_llm_suggestion` method extracts the suggestion from the response received from the LLM (Language Model). - -The `call_llm` method calls the LLM with a prompt and returns the response. - -**Note**: -- The FunctionOptimizer class is designed to be subclassed and extended to create specific optimizers for different types of problems. -- Subclasses of FunctionOptimizer must implement the `_step` and `default_propagator` methods. -- The FunctionOptimizer class provides a consistent interface and behavior for managing and updating parameters based on feedback. -- The class uses the LLM to generate suggestions for updating the parameters. -- The class includes methods for constructing prompts, extracting suggestions, and calling the LLM. - -**Output Example**: -{ - "reasoning": "In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.", - "answer": {}, - "suggestion": { - "a": 10 - } -} -### FunctionDef __init__(self, parameters, config_list) -**__init__**: The function of __init__ is to initialize an instance of the FunctionOptimizer class. - -**parameters**: -- parameters: A list of ParameterNode objects representing the trainable nodes in the computational graph. -- config_list: A list of configurations for the OpenAIWrapper. Default is None. -- *args: Additional positional arguments. -- propagator: An instance of the Propagator class. Default is None. -- objective: A string representing the objective of the optimization task. Default is None. -- ignore_extraction_error: A boolean indicating whether to ignore type conversion errors when extracting updated values from LLM's suggestion. Default is True. -- include_example: A boolean indicating whether to include an example problem and response in the prompt. Default is False. -- max_tokens: An integer representing the maximum number of tokens allowed in the prompt. Default is 4096. -- log: A boolean indicating whether to log the optimization process. Default is True. -- **kwargs: Additional keyword arguments. - -**Code Description**: The __init__ method of the FunctionOptimizer class initializes an instance of the class. It takes in various parameters such as parameters, config_list, *args, propagator, objective, ignore_extraction_error, include_example, max_tokens, log, and **kwargs. - -The method first calls the __init__ method of the superclass (Optimizer) to initialize the parameters and propagator attributes. It then sets the ignore_extraction_error attribute based on the provided ignore_extraction_error parameter. - -If the config_list parameter is None, it uses the autogen.config_list_from_json function to retrieve the configuration list from the "OAI_CONFIG_LIST" JSON file. It then initializes the llm attribute with an instance of the autogen.OpenAIWrapper class, passing the config_list as a parameter. - -The objective attribute is set to the provided objective parameter if it is not None, otherwise it is set to the default_objective attribute of the class. - -The example_problem attribute is initialized with a formatted string template that represents an example problem instance. It includes placeholders for the instruction, code, documentation, variables, constraints, inputs, others, outputs, and feedback sections. - -The example_response attribute is initialized with a formatted string that represents an example response to the problem instance. It includes placeholders for the reasoning, answer, and suggestion sections. - -The include_example, max_tokens, and log attributes are set based on the provided parameters. - -**Note**: -- The FunctionOptimizer class is a subclass of the Optimizer class. -- The parameters attribute represents the trainable nodes in the computational graph. -- The config_list attribute represents the configuration list for the OpenAIWrapper. -- The propagator attribute represents the propagator for the optimization process. -- The objective attribute represents the objective of the optimization task. -- The ignore_extraction_error attribute indicates whether to ignore type conversion errors when extracting updated values from LLM's suggestion. -- The include_example attribute indicates whether to include an example problem and response in the prompt. -- The max_tokens attribute represents the maximum number of tokens allowed in the prompt. -- The log attribute indicates whether to log the optimization process. - -**Output Example**: -``` -FunctionOptimizer( - parameters=[ParameterNode: (name, dtype=, data=value)], - config_list=[...], - propagator=Propagator(), - objective="...", - ignore_extraction_error=True, - include_example=False, - max_tokens=4096, - log=True, - ... -) -``` -*** -### FunctionDef default_propagator(self) -**default_propagator**: The function of default_propagator is to return the default Propagator object of the optimizer. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The default_propagator function is a method within the FunctionOptimizer class. Its primary purpose is to return an instance of the GraphPropagator class. When this method is called, it creates and returns a new GraphPropagator object. The GraphPropagator class, which is a subclass of the Propagator class, is designed to collect all the nodes seen in a path and compute the propagated feedback to the parent nodes based on the child node's description, data, and feedback. This method does not take any parameters and simply returns a new GraphPropagator instance, which can then be used by the optimizer for its propagation tasks. - -**Note**: This method is straightforward and does not require any parameters. It is designed to provide a default propagator for the optimizer, ensuring that the optimizer has a predefined mechanism for handling propagation tasks. - -**Output Example**: -```python -GraphPropagator() -``` -*** -### FunctionDef summarize(self) -**summarize**: The function of summarize is to aggregate feedback from all the parameters, construct variables and update others, and classify the root nodes into variables and others. - -**parameters**: -- self: The instance of the class. - -**Code Description**: -The `summarize` function is a method of the `FunctionOptimizer` class. It aggregates feedback from all the parameters by calling the `aggregate` method of the `propagator` object. The feedbacks are obtained from the trainable parameters by iterating over the `parameters` attribute of the class instance and filtering out the non-trainable nodes. The feedbacks are then summed up using the `sum` function. - -After aggregating the feedback, the function converts the resulting `TraceGraph` object into a `FunctionFeedback` object by calling the `node_to_function_feedback` function. This function processes the nodes in the `TraceGraph` object, categorizes them into roots, intermediates, and outputs, and populates the corresponding attributes of the `FunctionFeedback` object. - -Next, the function constructs variables and updates others based on the trainable nodes. It creates a dictionary called `trainable_param_dict` that maps the parameter names to their corresponding parameter objects. It then updates the `variables` attribute of the `summary` object by filtering the `roots` dictionary based on the keys present in the `trainable_param_dict`. Similarly, it updates the `inputs` attribute of the `summary` object by filtering the `roots` dictionary based on the keys not present in the `trainable_param_dict`. - -Finally, the function returns the `summary` object, which represents the aggregated feedback, variables, and inputs. - -The `summarize` function is called in the `_step` method of the `FunctionOptimizer` class. It is used to summarize the feedback from the trainable parameters and construct prompts for further processing. The `summarize` function relies on the `propagator` object and the `node_to_function_feedback` function to perform its tasks. - -**Note**: -- The `summarize` function assumes that the `propagator` object is correctly initialized and contains the necessary methods and attributes. -- The function assumes that the `parameters` attribute of the class instance contains the necessary trainable nodes. -- The `node_to_function_feedback` function should be defined and accessible within the project for the `summarize` function to work correctly. -- The resulting `summary` object represents the aggregated feedback, variables, and inputs from the trainable parameters. - -**Output Example**: -A possible return value of the `summarize` function could be a `FunctionFeedback` object with the following attributes: -- `graph`: [(0, "function_call_1"), (1, "function_call_2"), ...] -- `others`: {"node_name_1": (data_1, constraint_1), "node_name_2": (data_2, constraint_2), ...} -- `roots`: {"root_name_1": (data_1, constraint_1), "root_name_2": (data_2, constraint_2), ...} -- `output`: {"output_name_1": (data_1, constraint_1), "output_name_2": (data_2, constraint_2), ...} -- `user_feedback`: "User feedback string" -- `documentation`: {"node_name_1": "Node description 1", "node_name_2": "Node description 2", ...} -*** -### FunctionDef repr_node_value(node_dict) -**repr_node_value**: The function of repr_node_value is to generate a formatted string representation of the values in a given dictionary, excluding keys that contain the substring "__code". - -**parameters**: The parameters of this Function. -· node_dict: A dictionary where each key is a string and each value is a list, with the first element of the list being the value to be represented. - -**Code Description**: The repr_node_value function processes a dictionary (node_dict) and creates a list of formatted strings based on the dictionary's contents. It iterates over each key-value pair in the dictionary. For each pair, if the key does not contain the substring "__code", it appends a string to the list in the format "(type) key=value", where "type" is the type of the first element in the value list, and "key" and "value" are the key and the first element of the value list, respectively. If the key contains the substring "__code", it appends a string in the format "(code) key:value". Finally, the function joins all the strings in the list with newline characters and returns the resulting string. - -This function is utilized in the probelm_instance method of the FunctionOptimizer class. In this context, repr_node_value is called to generate string representations of various components of a summary object, such as variables, inputs, outputs, and others. These string representations are then used to construct a ProblemInstance object, which encapsulates the details of a problem instance in a structured format. - -**Note**: -- Ensure that the input dictionary (node_dict) has lists as values, with the first element of each list being the value to be represented. -- Keys containing the substring "__code" will be treated differently and formatted as "(code) key:value". - -**Output Example**: -Given the input dictionary: -{ - "var1": [10], - "var2": ["example"], - "func__code": ["def func(): pass"] -} -The function would return: -``` -(int) var1=10 -(str) var2=example -(code) func__code:def func(): pass -``` -*** -### FunctionDef repr_node_constraint(node_dict) -**repr_node_constraint**: The function of repr_node_constraint is to generate a formatted string representation of the constraints in a given node dictionary. - -**parameters**: The parameters of this Function. -· node_dict: A dictionary where keys are node identifiers and values are tuples containing node attributes. - -**Code Description**: The repr_node_constraint function processes a dictionary of nodes, where each key-value pair represents a node and its attributes. The function iterates through each item in the dictionary. For each key-value pair, it checks if the key does not contain the substring "__code". If this condition is met and the second element of the value tuple (v[1]) is not None, it appends a formatted string to a temporary list (temp_list). The formatted string includes the type of the first element of the value tuple (v[0]), the key, and the second element of the value tuple (v[1]). If the key contains the substring "__code" and the second element of the value tuple (v[1]) is not None, it appends a different formatted string to the temporary list, indicating that the key is related to code. Finally, the function joins all the strings in the temporary list with newline characters and returns the resulting string. - -This function is called by the probelm_instance method of the FunctionOptimizer class. In this context, repr_node_constraint is used to generate a string representation of the constraints in the summary.variables dictionary, which is then included in the ProblemInstance object. This ensures that the constraints are properly formatted and included in the problem instance's representation. - -**Note**: Ensure that the node_dict parameter is correctly structured, with each value being a tuple where the second element can be None or a meaningful value to be included in the output. - -**Output Example**: -``` -(int) node1: 10 -(str) node2: constraint_value -(code) node3__code: some_code -``` -*** -### FunctionDef probelm_instance(self, summary, mask) -**probelm_instance**: The function of probelm_instance is to generate a ProblemInstance object based on the provided summary and an optional mask. It encapsulates and formats the details of a problem instance for optimization tasks. - -**parameters**: -- summary: A summary object containing the necessary information for the problem instance. -- mask (optional): A list of strings specifying the sections to exclude from the ProblemInstance object. - -**Code Description**: The probelm_instance function takes a summary object and an optional mask as input. It first checks if a mask is provided, and if not, initializes it as an empty list. - -The function then creates a ProblemInstance object by passing the following parameters: -- instruction: The instruction for the problem instance, obtained from the summary object. -- code: A string representing the code to be executed. It is obtained by joining the values of the sorted summary.graph dictionary, excluding the sections specified in the mask. -- documentation: A string providing documentation for the code. It is obtained by joining the values of the summary.documentation dictionary, excluding the sections specified in the mask. -- variables: A string listing the variables involved in the problem. It is obtained by calling the repr_node_value function on the summary.variables dictionary, excluding the sections specified in the mask. -- constraints: A string outlining any constraints on the variables or the problem. It is obtained by calling the repr_node_constraint function on the summary.variables dictionary, excluding the sections specified in the mask. -- inputs: A string detailing the inputs required for the code. It is obtained by calling the repr_node_value function on the summary.inputs dictionary, excluding the sections specified in the mask. -- outputs: A string specifying the expected outputs of the code. It is obtained by calling the repr_node_value function on the summary.output dictionary, excluding the sections specified in the mask. -- others: A string for any additional information related to the problem. It is obtained by calling the repr_node_value function on the summary.others dictionary, excluding the sections specified in the mask. -- feedback: A string containing feedback on the problem instance. It is obtained from the summary.user_feedback attribute, excluding the sections specified in the mask. - -The ProblemInstance object is then returned. - -The probelm_instance function is utilized in the FunctionOptimizer class, specifically in its __init__ method and construct_prompt method. In the __init__ method, it is used to create an example problem instance using the ProblemInstance class. In the construct_prompt method, it is called to generate the problem instance string representation, which is included in the user prompt. - -**Note**: When using the probelm_instance function, ensure that the summary object is properly populated with the required information. The mask parameter can be used to exclude specific sections from the generated ProblemInstance object. - -**Output Example**: -``` -#Instruction -Optimize the function to achieve the desired output. - -#Code -y = add(x=a,y=b) -z = subtract(x=y, y=c) - -#Documentation -add: add x and y -subtract: subtract y from x - -#Variables -(int) a = 5 - -#Constraints -a: a > 0 - -#Inputs -(int) b = 1 -(int) c = 5 - -#Others -(int) y = 6 - -#Outputs -(int) z = 1 - -#Feedback: -The result of the code is not as expected. The result should be 10, but the code returns 1 -``` -*** -### FunctionDef construct_prompt(self, summary, mask) -**construct_prompt**: The function of construct_prompt is to construct the system and user prompts based on the provided summary and optional mask. - -**parameters**: -- summary: A summary object containing the necessary information for the problem instance. -- mask (optional): A list of strings specifying the sections to exclude from the ProblemInstance object. -- *args: Additional positional arguments. -- **kwargs: Additional keyword arguments. - -**Code Description**: The construct_prompt function is designed to generate system and user prompts for optimization tasks. It begins by creating a system prompt by concatenating the representation_prompt and output_format_prompt attributes, which provide a generic representation and output rules. - -Next, the function constructs a user prompt using the user_prompt_template attribute. It formats this template with a string representation of a problem instance, generated by calling the probelm_instance method with the provided summary and mask. This problem instance encapsulates and formats the details of the problem for the user prompt. - -If the include_example attribute is set to True, the function prepends an example problem and response to the user prompt. This is done by formatting the example_problem_template attribute with the example_problem and example_response attributes. - -Finally, the function appends the final_prompt attribute to the user prompt and returns both the system prompt and the user prompt. - -The construct_prompt function is called within the _step method of the FunctionOptimizer class. In this context, it is used to generate the necessary prompts for interacting with a language model, which then provides suggestions for optimizing the function. - -**Note**: Ensure that the summary object is properly populated with the required information before calling construct_prompt. The mask parameter can be used to exclude specific sections from the generated ProblemInstance object. - -**Output Example**: -``` -system_prompt: "Generic representation and output rules" -user_prompt: "Example problem and response (if include_example is True) + Problem instance details + Final prompt" -``` -*** -### FunctionDef _step(self, verbose, mask) -**_step**: The `_step` function is responsible for executing a single optimization step in the `FunctionOptimizer` class. It performs various operations such as summarizing feedback, constructing prompts, calling the language model, extracting suggestions, constructing an update dictionary, and logging the interaction. - -**parameters**: -- `self`: The instance of the `FunctionOptimizer` class. -- `verbose` (optional): A boolean indicating whether to print verbose output. Default is `False`. -- `mask` (optional): A list of strings specifying sections to exclude from the problem instance. Default is `None`. -- `*args`: Additional positional arguments. -- `**kwargs`: Additional keyword arguments. - -**Code Description**: -The `_step` function begins by asserting that the `propagator` attribute of the `FunctionOptimizer` instance is an instance of the `GraphPropagator` class. This ensures that the necessary methods and attributes are available for the subsequent operations. - -Next, the function calls the `summarize` method of the `FunctionOptimizer` class to aggregate feedback from all the parameters. This is done by invoking the `summarize` function defined in the `function_optimizer.py` file. The `summarize` function aggregates feedback by calling the `aggregate` method of the `propagator` object and processes the resulting `TraceGraph` object. - -After summarizing the feedback, the function constructs system and user prompts by calling the `construct_prompt` method of the `FunctionOptimizer` class. This method formats the prompts using the `representation_prompt`, `output_format_prompt`, and `user_prompt_template` attributes of the class. It also generates a problem instance string by calling the `problem_instance` method with the provided summary and mask. The prompts are then concatenated and stored in the `system_prompt` and `user_prompt` variables. - -The function proceeds to call the `call_llm` method of the `FunctionOptimizer` class to interact with a language model. This method sends the system and user prompts to the language model and retrieves the generated response. The response is stored in the `response` variable. - -If the response contains the string "TERMINATE", the function returns an empty dictionary. - -Otherwise, the function calls the `extract_llm_suggestion` method of the `FunctionOptimizer` class to extract a suggestion dictionary from the response. This method attempts to parse the response as JSON and retrieve the "suggestion" key. If the parsing fails, it falls back to extracting key-value pairs using regular expressions. The extracted suggestion dictionary is stored in the `suggestion` variable. - -The function then calls the `construct_update_dict` method of the `FunctionOptimizer` class to convert the suggestion into the appropriate data types. This method iterates over the parameters of the optimizer and checks if each parameter is trainable and if its name exists in the suggestion dictionary. If both conditions are met, it attempts to convert the suggestion value to the data type of the parameter using the `type` function. The parameter and its updated value are added to the `update_dict` dictionary. - -If the `log` attribute of the optimizer is not `None`, the function appends a dictionary containing the system prompt, user prompt, and response to the log. - -Finally, the function returns the `update_dict` dictionary, which maps `ParameterNode` objects to their corresponding updated values. - -The `_step` function is an essential part of the optimization process in the `FunctionOptimizer` class. It relies on the `summarize`, `construct_prompt`, `call_llm`, `extract_llm_suggestion`, and `construct_update_dict` methods to perform its tasks. The function assumes that the necessary methods and attributes are correctly initialized and accessible within the class. - -**Note**: -- The `summarize` function assumes that the `propagator` object is correctly initialized and contains the necessary methods and attributes. -- The `summarize` function assumes that the `parameters` attribute of the class instance contains the necessary trainable nodes. -- The `node_to_function_feedback` function should be defined and accessible within the project for the `summarize` function to work correctly. -- The resulting `summary` object represents the aggregated feedback, variables, and inputs from the trainable parameters. -- The `construct_prompt` function assumes that the summary object is properly populated with the required information before calling it. -- The `construct_prompt` function assumes that the `representation_prompt`, `output_format_prompt`, `user_prompt_template`, `example_problem_template`, `example_problem`, `example_response`, and `final_prompt` attributes are correctly initialized within the class. -- The `call_llm` function assumes that the `llm` object is correctly initialized and contains the necessary methods and attributes. -- The `extract_llm_suggestion` function assumes that the response string contains a "suggestion" key within a JSON object. -- The `construct_update_dict` function assumes that the `parameters` attribute exists and is a list of `ParameterNode` objects. -- The `construct_update_dict` function assumes that the suggestion dictionary contains the keys corresponding to the `py_name` attribute of the `ParameterNode` objects. -- If the suggestion is missing a key or the conversion fails, an exception is raised unless the `ignore_extraction_error` flag is set to `True`. -- The `_step` function assumes that the necessary methods and attributes are correctly initialized within the class. - -**Output Example**: -A possible return value of the `_step` function could be a dictionary mapping `ParameterNode` objects to their corresponding updated values: -``` -{ - : , - : , - ... -} -``` -*** -### FunctionDef construct_update_dict(self, suggestion) -**construct_update_dict**: The function of construct_update_dict is to convert the suggestion in text into the right data type. - -**parameters**: -- suggestion: A dictionary containing suggestions in text form. - - Type: Dict[str, Any] -- return: A dictionary mapping ParameterNode objects to their corresponding updated values. - - Type: Dict[ParameterNode, Any] - -**Code Description**: -The `construct_update_dict` function takes a suggestion in text form and converts it into the appropriate data type. It iterates over the `parameters` list of the current instance and checks if each parameter is trainable and if its name exists in the suggestion dictionary. If both conditions are met, it attempts to convert the suggestion value to the data type of the parameter using the `type` function. If the conversion is successful, the parameter and its updated value are added to the `update_dict` dictionary. - -In case the suggestion is missing the key or the conversion fails due to an incorrect data type, an exception is raised. However, if the `ignore_extraction_error` flag is set to True, a warning is issued instead of raising an exception. - -The `update_dict` dictionary, containing the ParameterNode objects and their updated values, is then returned as the output of the function. - -This function is called by the `_step` method of the `FunctionOptimizer` class in the `function_optimizer.py` file. In the `_step` method, the `construct_update_dict` function is used to convert the suggestion obtained from the language model into the appropriate data types for updating the parameters of the optimizer. - -**Note**: -- The `construct_update_dict` function assumes that the `parameters` attribute exists and is a list of ParameterNode objects. -- The `construct_update_dict` function assumes that the suggestion dictionary contains the keys corresponding to the `py_name` attribute of the ParameterNode objects. -- If the suggestion is missing a key or the conversion fails, an exception is raised unless the `ignore_extraction_error` flag is set to True. - -**Output Example**: -A possible return value of the `construct_update_dict` function could be a dictionary mapping ParameterNode objects to their corresponding updated values. For example: -``` -{ - : , - : , - ... -} -``` -*** -### FunctionDef extract_llm_suggestion(self, response) -**extract_llm_suggestion**: The function of extract_llm_suggestion is to extract a suggestion dictionary from a given response string. - -**parameters**: The parameters of this Function. -· response: A string containing the response from which the suggestion needs to be extracted. - -**Code Description**: The extract_llm_suggestion function is designed to parse a response string, typically from a language model, and extract a dictionary of suggestions. The function attempts to decode the response as JSON and retrieve the "suggestion" key. If the initial attempt fails due to a JSONDecodeError, the function tries to clean the response by extracting content within curly braces and attempts to decode it again. If the suggestion dictionary is still empty, the function uses a regular expression to manually extract key-value pairs from the response string. - -The function is called by the _step method within the same class, FunctionOptimizer. In the _step method, the extract_llm_suggestion function is used to process the response from a language model call and extract meaningful suggestions, which are then used to construct an update dictionary. This update dictionary is crucial for the subsequent steps in the optimization process. - -**Note**: -- The function makes two attempts to decode the response as JSON before resorting to regular expression parsing. -- If the suggestion dictionary remains empty after all attempts, the function prints an error message indicating the failure to extract suggestions. -- The function assumes that the response string contains a "suggestion" key within a JSON object. - -**Output Example**: -If the response string is '{"suggestion": {"param1": "value1", "param2": "value2"}}', the function will return: -``` -{ - "param1": "value1", - "param2": "value2" -} -``` -*** -### FunctionDef call_llm(self, system_prompt, user_prompt, verbose, max_tokens) -**call_llm**: The function of call_llm is to interact with a language model (LLM) using provided prompts and return the generated response. - -**parameters**: The parameters of this Function. -· system_prompt: A string representing the initial prompt given to the LLM, typically setting the context or instructions for the LLM. -· user_prompt: A string representing the user's input or query that follows the system prompt. -· verbose: A boolean or string parameter that controls the verbosity of the function. If set to True or "output", the prompts and responses are printed to the console. -· max_tokens: An integer specifying the maximum number of tokens the LLM should generate in its response. The default value is 4096. - -**Code Description**: The call_llm function is designed to facilitate communication with a language model by sending it a structured prompt and retrieving its response. The function first checks the verbosity setting; if verbose is set to True or "output", it prints the combined system and user prompts. It then constructs a message list with roles "system" and "user" to format the prompts appropriately for the LLM. - -The function attempts to generate a response from the LLM in JSON format. If this attempt fails, it falls back to a simpler response generation method, using the max_tokens parameter to limit the response length. The response content is extracted from the LLM's output and, if verbosity is enabled, printed to the console. Finally, the function returns the LLM's response content. - -This function is called by the _step method within the same module. The _step method uses call_llm to generate suggestions or updates based on the current state summarized by the system and user prompts. The response from call_llm is then processed to extract actionable suggestions, which are used to update the system's state. - -**Note**: -- Ensure that the LLM instance (self.llm) is properly initialized before calling this function. -- The verbose parameter can be used to debug or log the interaction with the LLM by printing the prompts and responses. -- Handle exceptions appropriately when the LLM fails to generate a JSON response. - -**Output Example**: -A possible return value of the function might look like: -``` -"Sure, I can help you with that. What specific information are you looking for?" -``` -*** -## ClassDef FunctionOptimizerV2 -**FunctionOptimizerV2**: The function of FunctionOptimizerV2 is to serve as an enhanced version of the FunctionOptimizer class, providing additional functionality and improvements to the optimization process. - -**attributes**: -- output_format_prompt: A string that defines the output format of the optimizer's response. -- example_problem_template: A string template for an example problem instance and response. -- user_prompt_template: A string template for the user prompt. -- example_prompt: A string that provides feasible but not optimal solutions for the current problem instance as a hint. -- final_prompt: A string template for the final prompt. - -**Code Description**: -The FunctionOptimizerV2 class is a subclass of the FunctionOptimizer class and provides an enhanced version of the optimization process. It extends the FunctionOptimizer class and overrides some of its methods to add additional functionality. - -The `__init__` method initializes the FunctionOptimizerV2 object by calling the superclass's `__init__` method and passing the arguments. It also initializes the `memory` attribute, which is a FIFOBuffer object used to store past variables and feedbacks. - -The `construct_prompt` method overrides the superclass's method to add examples from the memory to the user prompt. It checks if the memory is not empty and adds the variables and feedbacks from the memory to the user prompt. - -**Note**: -- The FunctionOptimizerV2 class is designed to enhance the optimization process by adding memory functionality. -- The class extends the FunctionOptimizer class and overrides some of its methods to add the desired functionality. -- The `memory` attribute stores past variables and feedbacks. -- The `construct_prompt` method adds examples from the memory to the user prompt. - -**Output Example**: -{ - "reasoning": "In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.", - "answer": {}, - "suggestion": { - "a": 10 - } -} -## ClassDef FunctionOptimizerV2Memory -**FunctionOptimizerV2Memory**: The function of FunctionOptimizerV2Memory is to enhance the optimization process by incorporating a memory mechanism that stores past variables and feedbacks. - -**attributes**: The attributes of this Class. -· memory: A FIFOBuffer object that stores past variables and feedbacks. - -**Code Description**: The FunctionOptimizerV2Memory class extends the FunctionOptimizerV2 class by adding a memory mechanism to the optimization process. This class is designed to improve the optimization process by utilizing past experiences stored in memory. - -The `__init__` method initializes the FunctionOptimizerV2Memory object. It calls the superclass's `__init__` method with the provided arguments and initializes the `memory` attribute as a FIFOBuffer object with a specified memory size. - -The `construct_prompt` method constructs the system and user prompts by calling the superclass's `construct_prompt` method. It then checks if the memory contains any past variables and feedbacks. If the memory is not empty, it adds these examples to the user prompt. The method splits the user prompt at the final prompt, adds a section containing past variables and feedbacks, and then reconstructs the user prompt. Finally, it adds the current summary's variables and user feedback to the memory. - -This class is used within the project to enhance the functionality of the FunctionOptimizerV2 by adding a memory component, which allows the optimizer to consider past experiences when constructing prompts. - -**Note**: -- The memory attribute is a FIFOBuffer that stores past variables and feedbacks. -- The construct_prompt method enhances the user prompt by including examples from the memory. -- This class is designed to improve the optimization process by leveraging past experiences. - -**Output Example**: -```json -{ - "system_prompt": "System prompt content here...", - "user_prompt": "User prompt content here...\nBelow are some variables and their feedbacks you received in the past.\n\n{\n \"variables\": {\n \"var1\": \"value1\",\n \"var2\": \"value2\"\n },\n \"feedback\": \"feedback content\"\n}\n\nFinal prompt content here..." -} -``` -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the FunctionOptimizerV2Memory class with optional memory size and other parameters. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· memory_size: An optional integer parameter that specifies the size of the FIFO buffer. Default is 0. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The __init__ method is the constructor for the FunctionOptimizerV2Memory class. It begins by calling the constructor of its superclass using `super().__init__(*args, **kwargs)`, ensuring that any initialization logic in the parent class is executed. Following this, it initializes a FIFOBuffer instance with the specified memory size by passing the `memory_size` parameter to the FIFOBuffer constructor. The FIFOBuffer is assigned to the `self.memory` attribute of the FunctionOptimizerV2Memory instance. - -The FIFOBuffer class, which is used here, manages a First-In-First-Out (FIFO) buffer of a specified size. This buffer is designed to store a limited number of items, automatically discarding the oldest items when new ones are added beyond its capacity. In the context of FunctionOptimizerV2Memory, the FIFOBuffer likely serves to maintain a history of optimization states or results, ensuring that only the most recent entries are kept. - -**Note**: -- Ensure that the `memory_size` parameter is a non-negative integer to avoid unexpected behavior. -- The FIFOBuffer will automatically discard the oldest items when new items are added beyond its capacity, maintaining the specified buffer size. -*** -### FunctionDef construct_prompt(self, summary, mask) -**construct_prompt**: The function of construct_prompt is to construct the system and user prompt. - -**parameters**: The parameters of this Function. -· summary: A summary object containing variables and user feedback. -· mask: An optional parameter to mask certain parts of the prompt. -· *args: Additional positional arguments. -· **kwargs: Additional keyword arguments. - -**Code Description**: The construct_prompt function is designed to create both system and user prompts by leveraging the functionality of its superclass. Initially, it calls the superclass's construct_prompt method to generate the base system and user prompts. - -If the memory buffer contains any entries, the function enhances the user prompt by adding examples from past interactions. It does this by splitting the user prompt at a predefined final prompt and then appending a formatted string that includes past variables and their corresponding feedback. These examples are formatted as JSON strings for clarity and are joined together with newline characters. - -After constructing the enhanced user prompt, the function adds the current summary's variables and user feedback to the memory buffer using the add method from the FIFOBuffer class. This ensures that the memory buffer is updated with the latest interaction, maintaining a record of past interactions for future use. - -**Note**: -- The memory buffer must be properly initialized and managed to ensure that past interactions are correctly stored and retrieved. -- Proper handling of the mask parameter is essential if masking functionality is required. -- The function relies on the superclass's construct_prompt method, so any changes to the superclass method may affect this function's behavior. - -**Output Example**: -A possible return value of the function could be: -``` -system_prompt: "System prompt content" -user_prompt: "User prompt content\nBelow are some variables and their feedbacks you received in the past.\n\n{\n \"variables\": {\n \"var1\": \"value1\",\n \"var2\": \"value2\"\n },\n \"feedback\": \"positive\"\n}\n\nFinal prompt content" -``` -*** diff --git a/generated_docs/opto/optimizers/opro.md b/generated_docs/opto/optimizers/opro.md deleted file mode 100644 index 4178cbad..00000000 --- a/generated_docs/opto/optimizers/opro.md +++ /dev/null @@ -1,79 +0,0 @@ -## ClassDef OPRO -**OPRO**: The function of OPRO is to serve as a subclass of the FunctionOptimizer class, implementing the optimization process for a specific problem. It overrides the `_step` method to propose new parameter values based on feedback and constructs the update dictionary. It also provides methods for constructing prompts, extracting suggestions, and calling the Language Model (LLM). - -**attributes**: -- user_prompt_template: A template for the user prompt, including placeholders for the problem instance and the instruction. -- output_format_prompt: A template for the output format of the optimizer's response, specifying the JSON format and providing a structure for the response. -- default_objective: The default objective of the optimizer, which is to change the values of the variables in the `#Variables` section to improve the output according to the feedback. -- buffer: A list used to store the variables and feedback from each step of the optimization process. - -**Code Description**: -The OPRO class is a subclass of the FunctionOptimizer class and provides a specific implementation for optimizing a problem. It extends the FunctionOptimizer class and overrides the `_step` method to propose new parameter values based on feedback and construct the update dictionary. - -The `__init__` method initializes the OPRO object by calling the superclass's `__init__` method and passing the arguments. It also initializes the `buffer` attribute as an empty list. - -The `construct_prompt` method constructs the system and user prompts based on the summary and a mask. It uses the `user_prompt_template` attribute to format the user prompt, including the problem instance and the instruction. - -The `_step` method is responsible for proposing new parameter values based on feedback. It calls the LLM with the system and user prompts and extracts the suggestion from the response. It then constructs the update dictionary using the `construct_update_dict` method. - -The `construct_update_dict` method converts the suggestion in text format into the right data type and constructs an update dictionary. It iterates over the trainable parameters and checks if the parameter is present in the suggestion. If it is, it tries to convert the suggestion value to the data type of the parameter and adds it to the update dictionary. - -The `extract_llm_suggestion` method extracts the suggestion from the response received from the LLM. It first tries to parse the response as a JSON object and extract the suggestion from the "suggestion" field. If that fails, it tries to extract the suggestion key-value pairs using regular expressions. - -The `call_llm` method calls the LLM with a prompt and returns the response. It formats the prompt as a list of messages with system and user roles and calls the LLM's `create` method. It then retrieves the response from the LLM's `choices` attribute. - -**Note**: -- The OPRO class is designed to be subclassed and extended to create specific optimizers for different types of problems. -- Subclasses of OPRO must implement the `_step` method. -- The OPRO class provides methods for constructing prompts, extracting suggestions, and calling the LLM. -- The class uses the FunctionOptimizer class as its superclass and inherits its attributes and methods. - -**Output Example**: -{ - "reasoning": "In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.", - "answer": {}, - "suggestion": { - "a": 10 - } -} -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the OPRO class. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The __init__ method is a constructor that initializes an instance of the OPRO class. It begins by calling the __init__ method of its superclass using the super() function, passing along any arguments (*args) and keyword arguments (**kwargs) it received. This ensures that the parent class is properly initialized. After the superclass initialization, it creates an instance variable named 'buffer' and initializes it as an empty list. This 'buffer' can be used to store data or objects that are relevant to the instance of the OPRO class. - -**Note**: -- Ensure that the superclass of OPRO is correctly defined and its __init__ method is compatible with the arguments passed. -- The 'buffer' list is initialized as empty and can be used to store any necessary data during the lifecycle of the OPRO instance. -*** -### FunctionDef construct_prompt(self, summary, mask) -**construct_prompt**: The function of construct_prompt is to construct the system and user prompt based on the provided summary and optional mask. - -**parameters**: The parameters of this Function. -· summary: An object containing variables and user feedback. -· mask: An optional parameter that can be used to filter or modify the prompt construction process. -· *args: Additional positional arguments. -· **kwargs: Additional keyword arguments. - -**Code Description**: The construct_prompt function begins by appending a tuple of summary variables and user feedback to the buffer. It then iterates over the buffer to create a list of examples. Each example is a JSON-formatted string that includes the variables and feedback. The variables are formatted such that only the first element of each variable's value is included. These examples are joined into a single string with newline characters separating them. - -Next, the function constructs the user prompt by formatting the user_prompt_template with the examples and the objective. Finally, it returns a tuple containing the output_format_prompt and the constructed user prompt. - -**Note**: -- Ensure that the summary object contains the necessary attributes: variables and user_feedback. -- The buffer is assumed to be an attribute of the class instance and should be initialized before calling this function. -- The user_prompt_template and objective should also be defined as attributes of the class instance. - -**Output Example**: -Assuming the buffer contains two entries with the following data: -1. variables: {'var1': ['value1'], 'var2': ['value2']} - feedback: 'Good' -2. variables: {'var3': ['value3'], 'var4': ['value4']} - feedback: 'Needs improvement' - -The returned tuple might look like: -('output_format_prompt_value', 'User prompt with examples:\n{\n "variables": {\n "var1": "value1",\n "var2": "value2"\n },\n "feedback": "Good"\n}\n{\n "variables": {\n "var3": "value3",\n "var4": "value4"\n },\n "feedback": "Needs improvement"\n}\nInstruction: objective_value') -*** diff --git a/generated_docs/opto/optimizers/optimizers.md b/generated_docs/opto/optimizers/optimizers.md deleted file mode 100644 index 570bd128..00000000 --- a/generated_docs/opto/optimizers/optimizers.md +++ /dev/null @@ -1,267 +0,0 @@ -## ClassDef AbstractOptimizer -**AbstractOptimizer**: The function of AbstractOptimizer is to serve as a base class for optimizers, responsible for updating parameters based on feedback. - -**attributes**: The attributes of this Class. -· parameters: A list of ParameterNode objects that the optimizer will manage and update. - -**Code Description**: The AbstractOptimizer class is designed to be a foundational class for creating various optimizers. It ensures that any derived optimizer class will have a consistent interface and behavior for managing and updating parameters. - -- The `__init__` method initializes the optimizer with a list of ParameterNode objects. It asserts that the provided parameters are indeed a list and that each element in the list is an instance of ParameterNode. This ensures type safety and consistency in the parameters being managed. - -- The `step` method is an abstract method intended to be overridden by subclasses. It is supposed to contain the logic for updating the parameters based on feedback. Since it is not implemented in AbstractOptimizer, any subclass must provide an implementation for this method. - -- The `zero_feedback` method is another abstract method that must be implemented by subclasses. It is intended to reset the feedback for all parameters, preparing them for the next optimization step. - -- The `propagator` property is designed to return a Propagator object, which can be used to propagate feedback backward through the network. This property must also be implemented by any subclass. - -The AbstractOptimizer class is called by the Optimizer class, which extends its functionality. The Optimizer class provides concrete implementations for the abstract methods defined in AbstractOptimizer. For instance, it implements the `step` method to propose new parameter values based on feedback and then update the parameters accordingly. It also provides a `zero_feedback` method to reset feedback for all parameters and a `propagator` property to return the appropriate Propagator object. - -**Note**: -- Any subclass of AbstractOptimizer must implement the `step`, `zero_feedback`, and `propagator` methods. -- The parameters passed to the AbstractOptimizer must be a list of ParameterNode instances. -- The class ensures a consistent interface for optimizers, making it easier to extend and create new optimization algorithms. -### FunctionDef __init__(self, parameters) -**__init__**: The function of __init__ is to initialize an instance of the AbstractOptimizer class with a list of ParameterNode objects. - -**parameters**: The parameters of this Function. -· parameters: A list of ParameterNode objects that represent the parameters to be optimized. -· *args: Additional positional arguments. -· **kwargs: Additional keyword arguments. - -**Code Description**: The __init__ method of the AbstractOptimizer class is responsible for initializing the optimizer with a set of parameters. It takes a list of ParameterNode objects as its primary argument. The method first asserts that the provided parameters argument is indeed a list. It then checks that every element in this list is an instance of the ParameterNode class. If these conditions are met, the parameters are assigned to the instance variable self.parameters. - -The ParameterNode class, which is used in this context, represents a trainable node in a computational graph. It is initialized with various attributes such as value, name, trainable status, description, constraint, and additional info. The ParameterNode class inherits from a generic Node class and adds itself to a set of dependencies upon initialization. - -**Note**: -- Ensure that the parameters argument passed to the __init__ method is a list of ParameterNode objects. -- The method uses assertions to enforce type checking, which will raise an AssertionError if the conditions are not met. -- Additional positional and keyword arguments (*args and **kwargs) are accepted but not utilized within this method. -*** -### FunctionDef step(self) -**step**: The function of step is to update the parameters based on the feedback. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The step function is designed to update the parameters of an optimizer based on feedback. However, in its current form, it is an abstract method, meaning it is intended to be overridden by subclasses of the AbstractOptimizer class. The method raises a NotImplementedError, which indicates that any subclass must provide its own implementation of the step method. This design enforces that the specific logic for updating parameters must be defined in the subclasses, ensuring that the AbstractOptimizer class remains flexible and adaptable to various optimization strategies. - -**Note**: -- This method must be implemented in any subclass of AbstractOptimizer. -- Attempting to call this method directly from an instance of AbstractOptimizer will result in a NotImplementedError. -- Ensure that the subclass provides a concrete implementation of the step method to perform the actual parameter update logic. -*** -### FunctionDef zero_feedback(self) -**zero_feedback**: The function of zero_feedback is to reset the feedback. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The zero_feedback function is designed to reset the feedback mechanism within an optimizer. However, the function is currently not implemented and raises a NotImplementedError when called. This indicates that any subclass inheriting from the class containing this function must provide its own implementation of the zero_feedback method. The purpose of this function is to ensure that subclasses define how the feedback should be reset, which is crucial for the proper functioning of the optimizer. - -**Note**: When using this function, it is important to implement the zero_feedback method in any subclass that inherits from the parent class. Failure to do so will result in a NotImplementedError being raised, which will halt the execution of the program. This function serves as a placeholder to enforce the implementation of feedback resetting logic in derived classes. -*** -### FunctionDef propagator(self) -**propagator**: The function of propagator is to return a Propagator object that can be used to propagate feedback in backward. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The propagator function is designed to return a Propagator object, which is intended to be used for propagating feedback in a backward pass. However, the current implementation of this function raises a NotImplementedError. This indicates that the function is meant to be overridden in a subclass, where the actual logic for returning a Propagator object should be provided. The NotImplementedError serves as a placeholder to remind developers that they need to implement this method in any concrete subclass derived from the abstract class. - -**Note**: When using this function, ensure that it is properly overridden in any subclass. Attempting to call this method directly from the abstract class without overriding it will result in a NotImplementedError. -*** -## ClassDef Optimizer -**Optimizer**: The function of Optimizer is to serve as a base class for optimizers, responsible for updating parameters based on feedback. - -**attributes**: -- parameters: A list of ParameterNode objects that the optimizer will manage and update. - -**Code Description**: -The Optimizer class is a base class for creating various optimizers. It provides a consistent interface and behavior for managing and updating parameters based on feedback. The class extends the AbstractOptimizer class and implements the abstract methods defined in it. - -The `__init__` method initializes the optimizer with a list of ParameterNode objects. It ensures that the provided parameters are a list and that each element in the list is an instance of ParameterNode. This ensures type safety and consistency in the parameters being managed. The method also sets the propagator attribute to the default propagator returned by the default_propagator method. - -The `propagator` property returns the propagator object associated with the optimizer. - -The `step` method is responsible for proposing new parameter values based on feedback and updating the parameters accordingly. It calls the `propose` method to get the proposed update dictionary and then calls the `update` method to update the trainable parameters with the new data. - -The `propose` method is a helper method that calls the `_step` method to get the new data of the parameters based on the feedback. - -The `update` method updates the trainable parameters with the new data provided in the update dictionary. It iterates over the items in the update dictionary and updates the data of each trainable parameter if it is marked as trainable. - -The `zero_feedback` method resets the feedback for all parameters by calling the `zero_feedback` method of each parameter. - -The `_step` method is an abstract method that must be implemented by subclasses. It returns the new data of parameter nodes based on the feedback. Subclasses should provide their own implementation of this method. - -The `default_propagator` method is an abstract method that must be implemented by subclasses. It returns the default Propagator object of the optimizer. Subclasses should provide their own implementation of this method. - -The `backward` method propagates the feedback backward by calling the `backward` method of the given node with the propagator object. - -**Note**: -- Any subclass of Optimizer must implement the `_step`, `default_propagator`, and `backward` methods. -- The parameters passed to the Optimizer must be a list of ParameterNode instances. -- The class ensures a consistent interface for optimizers, making it easier to extend and create new optimization algorithms. - -**Output Example**: -```python -{ - 'parameter1': value1, - 'parameter2': value2, - ... -} -``` -### FunctionDef __init__(self, parameters) -**__init__**: The function of __init__ is to initialize an instance of the Optimizer class with specified parameters and an optional propagator. - -**parameters**: The parameters of this Function. -· parameters: A list of ParameterNode objects that represent the parameters to be optimized. -· *args: Additional positional arguments. -· propagator: An optional Propagator object. If not provided, a default Propagator will be used. -· **kwargs: Additional keyword arguments. - -**Code Description**: The __init__ method initializes an Optimizer instance. It first calls the superclass's __init__ method with the provided parameters. Then, it checks if a propagator is provided. If not, it calls the default_propagator method to obtain a default Propagator. The method ensures that the propagator is an instance of the Propagator class. Finally, it assigns the propagator to the instance's _propagator attribute. This setup ensures that the Optimizer always has a valid Propagator, either provided explicitly or obtained through the default_propagator method. - -**Note**: When using this class, ensure that the parameters argument is a list of ParameterNode objects and that the propagator, if provided, is an instance of the Propagator class. If no propagator is provided, the default_propagator method must be properly implemented in a subclass to avoid a NotImplementedError. -*** -### FunctionDef propagator(self) -**propagator**: The function of propagator is to return the internal `_propagator` attribute of the class. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The `propagator` function is a simple accessor method that returns the value of the `_propagator` attribute from the class instance. This method does not take any parameters and directly provides access to the internal `_propagator` attribute, which is presumably an instance of a propagator object used within the class. - -The `propagator` function is utilized in several other methods within the project. For instance, in the `summarize` method of the `FunctionOptimizer` class, it is used to aggregate feedback from all trainable parameters. The `propagator` is called to perform the aggregation of feedbacks, which are then summed up to create a summary. - -In the `_step` method of the `FunctionOptimizer` class, the `propagator` is asserted to be an instance of `GraphPropagator` before summarizing the feedback and constructing prompts for further processing. - -Additionally, in the `backward` method of the `Optimizer` class, the `propagator` is passed as an argument to the `backward` method of a node, facilitating the backward propagation of feedback. - -**Note**: This function is a straightforward accessor and does not perform any additional logic or validation. It is essential that the `_propagator` attribute is correctly initialized within the class for this method to function as expected. - -**Output Example**: The return value of the `propagator` function would be the internal `_propagator` object, which could be an instance of a class responsible for propagating information or feedback within the optimization process. For example: -``` - -``` -*** -### FunctionDef step(self) -**step**: The function of step is to execute a single optimization step by proposing new parameter data and updating the parameters accordingly. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The step function is a method within the Optimizer class that orchestrates the process of updating the trainable parameters. It performs this task in two main stages: - -1. **Propose New Data**: The function first calls the propose method, passing along any positional and keyword arguments it receives. The propose method generates a dictionary (update_dict) containing new data for the parameters. This dictionary is created based on feedback and is essential for the subsequent update process. - -2. **Update Parameters**: After obtaining the update_dict from the propose method, the step function calls the update method. The update method takes the update_dict as input and iterates over its key-value pairs. For each pair, it checks if the parameter node (key) is marked as trainable. If the node is trainable, it updates the node's internal data (_data) with the new data provided in the dictionary. - -The step function is integral to the optimization process, as it ensures that the parameters are updated based on the latest feedback. It relies on the propose method to generate the necessary updates and the update method to apply these updates to the parameters. - -**Note**: -- The propose method must be correctly implemented to generate a valid update_dict. -- The update method will only modify the parameters that are marked as trainable. -- The step function is designed to be flexible, accepting any number of positional and keyword arguments, which are passed through to the propose method. -*** -### FunctionDef propose(self) -**propose**: The function of propose is to propose the new data of the parameters based on the feedback. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The propose function is a method within the Optimizer class designed to generate new parameter data based on feedback. It serves as a public interface for proposing updates to the parameters. The function accepts any number of positional and keyword arguments, which are then passed directly to the _step method. - -The propose method internally calls the _step method, which is responsible for the actual computation of the new parameter data. The _step method is abstract and must be implemented by any subclass of the Optimizer class. This design allows for different optimization strategies to be implemented by overriding the _step method in subclasses. - -The propose method is also called by the step method within the same class. The step method uses propose to generate the update dictionary, which is then applied to update the parameters. - -**Note**: -- The _step method must be implemented in any subclass of the Optimizer class; otherwise, a NotImplementedError will be raised. -- The propose method relies on the _step method to perform the actual parameter updates, making it essential to provide a correct and efficient implementation of _step in subclasses. -- The function is designed to be flexible, accepting any number of positional and keyword arguments. - -**Output Example**: A possible appearance of the code's return value could be a dictionary where keys are instances of ParameterNode and values can be of any type, representing the new data for each parameter node. -*** -### FunctionDef update(self, update_dict) -**update**: The function of update is to update the trainable parameters given a dictionary of new data. - -**parameters**: The parameters of this Function. -· update_dict: A dictionary where keys are instances of ParameterNode and values are the new data to update the parameters with. - -**Code Description**: The update function is designed to modify the trainable parameters of an optimizer. It takes a dictionary, update_dict, as input. The keys of this dictionary are instances of ParameterNode, and the values are the new data to be assigned to these nodes. - -The function iterates over each key-value pair in the update_dict. For each pair, it checks if the ParameterNode (key) is marked as trainable. If the node is trainable, it updates the node's internal data (_data) with the new data provided in the dictionary. - -This function is called by the step function within the same Optimizer class. The step function first generates an update_dict by calling the propose method and then passes this dictionary to the update function to apply the updates. - -**Note**: -- Ensure that the keys in the update_dict are instances of ParameterNode. -- Only the nodes marked as trainable will be updated. -- This function directly modifies the internal state (_data) of the ParameterNode instances. -*** -### FunctionDef zero_feedback(self) -**zero_feedback**: The function of zero_feedback is to reset the feedback values of all parameters managed by the optimizer to zero. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The zero_feedback function iterates over all the parameters contained within the optimizer instance and calls the zero_feedback method on each parameter. This effectively resets any feedback-related values or states associated with the parameters to zero. This function is crucial in scenarios where feedback mechanisms are used to adjust parameters during optimization, and there is a need to reset these adjustments, possibly at the beginning of a new optimization cycle or after a certain number of iterations. - -The function is called within the context of unit tests located in tests\unit_tests\test_optimizer.py, indicating its importance in ensuring that the feedback resetting mechanism works correctly. This is essential for maintaining the integrity and expected behavior of the optimizer during its operation. - -**Note**: -- Ensure that each parameter object within the optimizer has a zero_feedback method implemented; otherwise, this function will raise an AttributeError. -- This function should be used when there is a need to clear feedback states, typically before starting a new optimization phase or after specific intervals to maintain the stability and performance of the optimization process. -*** -### FunctionDef _step(self) -**_step**: The function of _step is to return the new data of parameter nodes based on the feedback. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The _step function is designed to be a core method within an optimizer class, responsible for updating the data of parameter nodes based on feedback. This function is abstract and raises a NotImplementedError, indicating that any subclass must provide an implementation for this method. The return type of the function is a dictionary where keys are instances of ParameterNode and values can be of any type. - -The _step function is called by the propose method within the same class. The propose method serves as a public interface to generate new parameter data based on feedback, and it delegates the actual computation to the _step method. This design allows for flexibility and extensibility, as different optimization strategies can be implemented by overriding the _step method in subclasses. - -The ParameterNode class, which is referenced in the return type, represents a trainable node in a computational graph. It inherits from a generic Node class and includes additional attributes such as name, trainable status, description, constraint, and info. The ParameterNode class also maintains a set of dependencies, specifically adding itself to a 'parameter' dependency set. - -**Note**: -- The _step function must be implemented in any subclass of the optimizer class; otherwise, a NotImplementedError will be raised. -- The function is designed to be flexible, accepting any number of positional and keyword arguments. -- The propose method relies on _step to perform the actual parameter updates, making it essential to provide a correct and efficient implementation of _step in subclasses. -*** -### FunctionDef default_propagator(self) -**default_propagator**: The function of default_propagator is to return the default Propagator object of the optimizer. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The default_propagator function is designed to return the default Propagator object associated with the optimizer. However, in its current implementation, it raises a NotImplementedError. This indicates that the function is intended to be overridden in a subclass, where the actual logic for returning a default Propagator will be provided. The function is called within the __init__ method of the Optimizer class. During the initialization of an Optimizer object, if no Propagator is explicitly provided, the default_propagator function is invoked to obtain a default Propagator. The returned Propagator is then assigned to the _propagator attribute of the Optimizer instance. This ensures that the Optimizer always has a valid Propagator, either provided explicitly or obtained through the default_propagator method. - -**Note**: When implementing a subclass of the Optimizer, it is essential to override the default_propagator method to provide a concrete implementation that returns a valid Propagator object. Failure to do so will result in a NotImplementedError being raised during the initialization of the Optimizer if no Propagator is provided. -*** -### FunctionDef backward(self, node) -**backward**: The function of backward is to perform a backward pass in the optimization process. It propagates feedback from a node to its parents by calling the propagator function and updating the feedback values. - -**parameters**: -- node: The node from which the feedback is propagated. -- *args: Additional positional arguments that can be passed to the node's backward method. -- **kwargs: Additional keyword arguments that can be passed to the node's backward method. - -**Code Description**: The backward function is responsible for propagating feedback from a node to its parents in the optimization process. It first checks if a propagator function is provided, and if not, it imports the GraphPropagator class from the opto.trace.propagators.graph_propagator module. - -The function then adds the feedback from the node to a feedback dictionary using the _add_feedback method of the node. The feedback is obtained by calling the propagator function with the node as an argument. The feedback dictionary is used to store the feedback from each child node, where each key is a child node and the value is a list of feedbacks from that child. - -After adding the feedback, the function iterates over the parents of the node and propagates the feedback to each parent. If a parent is present in the propagated feedback dictionary, the feedback is added to the parent using the _add_feedback method. - -The function also supports visualization of the propagation process by creating a graph using the graphviz library. The graph is created in reverse order if the reverse_plot parameter is set to True. - -Finally, the function sets the _backwarded attribute of the node to True, indicating that the backward pass has been performed. The value of the retain_graph parameter determines whether the feedback should be retained or zeroed out after propagation. - -**Note**: It is important to ensure that the propagator function is correctly initialized before calling the backward function. The function relies on the propagator to perform the feedback propagation. If the propagator is not provided or initialized correctly, the backward pass may not function as expected. - -**Output Example**: The backward function returns a graph (digraph) object if the visualize parameter is set to True. Otherwise, it returns None. -*** diff --git a/generated_docs/opto/trace/broadcast.md b/generated_docs/opto/trace/broadcast.md deleted file mode 100644 index bb0367d8..00000000 --- a/generated_docs/opto/trace/broadcast.md +++ /dev/null @@ -1,54 +0,0 @@ -## FunctionDef apply_op(op, output) -**apply_op**: The function of apply_op is to perform a broadcasting operation that applies a given operator to a container of Nodes. - -**parameters**: -- op (callable): The operator to be applied. -- output (Any): The container to be updated. -- *args (Any): The positional inputs of the operator. -- **kwargs (Any): The keyword inputs of the operator. - -**Code Description**: -The apply_op function takes an operator (op), an output container, and positional and keyword inputs. It first combines the positional and keyword inputs into a single list called "inputs". It then checks if there are any containers in the inputs list. If there are no containers, indicating that all inputs are Nodes, the function simply applies the operator to the inputs and returns the result. - -If there is at least one container in the inputs list, the function performs the broadcasting operation. It iterates over the output container and applies the operator recursively to each element of the output container, along with the corresponding elements from the positional and keyword inputs. The result of each recursive call is assigned back to the corresponding element in the output container. - -The function handles different types of output containers: -- If the output is a list or tuple, the function checks that the output and inputs have the same length. It then applies the operator to each element of the output container, along with the corresponding elements from the positional and keyword inputs. -- If the output is a dictionary, the function iterates over the key-value pairs of the output and applies the operator to each value, along with the corresponding elements from the positional and keyword inputs. -- If the output is an instance of the NodeContainer class, the function iterates over the attributes of the output and applies the operator to each attribute, along with the corresponding elements from the positional and keyword inputs. - -The apply_op function ensures that all inputs are either Nodes or have the same type as the output. It raises an assertion error if this condition is not met. - -**Note**: -- The apply_op function relies on the NodeContainer class to identify containers of Nodes and apply the operator recursively to each attribute of the container. -- The function supports broadcasting operations on different types of output containers, including lists, tuples, dictionaries, and instances of the NodeContainer class. -- It is important to ensure that the inputs and output are compatible in terms of length and type to avoid errors during the broadcasting operation. - -**Output Example**: -The updated output container after applying the operator to the inputs. -### FunctionDef admissible_type(x, base) -**admissible_type**: The function of admissible_type is to determine whether the type of an object is admissible for a given base type or if it is an instance of the Node class. - -**parameters**: -- x: The object whose type needs to be checked. -- base: The base type against which the object's type is compared. - -**Code Description**: -The admissible_type function takes two parameters, x and base, and returns a boolean value indicating whether the type of x is equal to the type of base or if x is an instance of the Node class. - -The function first checks if the type of x is equal to the type of base using the "type" function. If the types are equal, it returns True. - -If the types are not equal, the function uses the "isinstance" function to check if x is an instance of the Node class. If x is an instance of Node, it returns True. Otherwise, it returns False. - -This function is useful when you want to check if an object's type is admissible for a specific base type or if it is an instance of a specific class. - -**Note**: -- The function assumes that the Node class is defined and imported correctly. -- The function only checks for exact type equality, not inheritance relationships. - -**Output Example**: -- admissible_type(5, int) returns True -- admissible_type("hello", str) returns True -- admissible_type(5, str) returns False -- admissible_type(Node(), Node) returns True -*** diff --git a/generated_docs/opto/trace/bundle.md b/generated_docs/opto/trace/bundle.md deleted file mode 100644 index 611765cd..00000000 --- a/generated_docs/opto/trace/bundle.md +++ /dev/null @@ -1,469 +0,0 @@ -## FunctionDef bundle(description, n_outputs, node_dict, traceable_code, wrap_output, unpack_input, trainable, catch_execution_error, allow_external_dependencies, overwrite_python_recursion) -**bundle**: The function of bundle is to wrap a function as a FunModule, which returns node objects. - -**parameters**: -- description: A string that describes the function. -- n_outputs: An integer that specifies the number of outputs the wrapped function should have. -- node_dict: Either "auto" or a dictionary that maps input names to node objects. -- traceable_code: A boolean value indicating whether the code should be traced using nodes. -- wrap_output: A boolean value indicating whether the output should be wrapped as a node object. -- unpack_input: A boolean value indicating whether the input should be unpacked. -- trainable: A boolean value indicating whether the wrapped function is trainable. -- catch_execution_error: A boolean value indicating whether execution errors should be caught. -- allow_external_dependencies: A boolean value indicating whether external dependencies are allowed. -- overwrite_python_recursion: A boolean value indicating whether Python recursion should be overwritten. - -**Code Description**: The bundle function is a decorator that wraps a function as a FunModule. It takes in various parameters to customize the behavior of the wrapped function. Inside the decorator, it creates a FunModule object with the specified parameters and returns it. - -The decorator function also captures the locals of the calling function using the inspect module. This allows the wrapped function to access the locals of the calling function. - -The wrapped function can be called with the same input signature as the original function. The output of the wrapped function is a node object, which represents the result of the function computation. The node object can be used in further computations or as inputs to other functions. - -The bundle function provides flexibility in customizing the behavior of the wrapped function. It allows specifying the number of outputs, mapping input names to node objects, tracing the code using nodes, wrapping the output as a node object, unpacking the input, making the wrapped function trainable, catching execution errors, allowing external dependencies, and overwriting Python recursion. - -**Note**: -- The wrapped function should have a consistent input signature. -- The wrapped function can access the locals of the calling function. -- The output of the wrapped function is a node object. -- The behavior of the wrapped function can be customized using the parameters of the bundle function. - -**Output Example**: -```python -@bundle(description="This is a bundled function", n_outputs=2) -def add(a, b): - return a + b, a - b - -output = add(3, 2) -print(output) -# Output: (5, 1) -``` -### FunctionDef decorator(fun) -Doc is waiting to be generated... -*** -## ClassDef trace_nodes -**trace_nodes**: The function of trace_nodes is to act as a context manager for tracking which nodes are read or used in an operator. - -**attributes**: The attributes of this Class. -· No explicit attributes are defined within this class. - -**Code Description**: The trace_nodes class is designed to manage the tracking of nodes that are accessed during the execution of an operator. It achieves this by leveraging Python's context management protocol, which includes the `__enter__` and `__exit__` methods. - -- The `__enter__` method initializes a new set to store the nodes that will be used and appends this set to the global `USED_NODES` list. It then returns this set, allowing it to be used within the context. -- The `__exit__` method is called when the context is exited. It removes the set of used nodes from the global `USED_NODES` list, ensuring that the tracking is properly cleaned up. - -In the context of its usage within the `forward` method of the `FunModule` class, the trace_nodes context manager is used to keep track of all nodes that are accessed during the execution of the operator function (`self.fun`). When the `forward` method is called, it enters the trace_nodes context, which starts tracking the nodes. After the function execution, the context is exited, and the set of used nodes is then available for further processing. - -The `forward` method uses this set of nodes to construct the inputs of a `MessageNode` from the function inputs or the set of used nodes. It also identifies any external dependencies, which are nodes used to create the outputs but not included in the inputs. If external dependencies are not allowed and are detected, an exception is raised. - -**Note**: -- Ensure that the global `USED_NODES` list is properly managed to avoid any unintended side effects. -- The trace_nodes context manager should be used within a controlled environment where the global state can be safely modified and restored. - -**Output Example**: -When used within the `forward` method of the `FunModule` class, the trace_nodes context manager might return a set of nodes that were accessed during the function execution. For example: -``` -with trace_nodes() as used_nodes: - # Function execution that accesses nodes - pass -# used_nodes might contain: {Node1, Node2, Node3} -``` -### FunctionDef __enter__(self) -**__enter__**: The function of __enter__ is to initialize and return a new set of nodes, and to append this set to the global list USED_NODES. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the class in which this method is defined. - -**Code Description**: The __enter__ method is a special method used in the context management protocol. When an instance of the class containing this method is used in a with statement, the __enter__ method is automatically invoked at the beginning of the block. In this implementation, the method performs the following actions: -1. Initializes an empty set named `nodes`. -2. Appends this set to the global list `USED_NODES`. -3. Returns the set `nodes`. - -This allows the set of nodes to be used within the with block and ensures that it is tracked in the global `USED_NODES` list. - -**Note**: -- Ensure that the global list `USED_NODES` is defined before using this method. -- This method is typically paired with an `__exit__` method to handle cleanup actions when the with block is exited. - -**Output Example**: -When the __enter__ method is called, it returns an empty set. For example: -``` -with some_instance as nodes: - # nodes is an empty set - print(nodes) # Output: set() -``` -*** -### FunctionDef __exit__(self, type, value, traceback) -**__exit__**: The function of __exit__ is to handle the cleanup process when exiting a context managed by a with statement. - -**parameters**: The parameters of this Function. -· type: The exception type, if an exception was raised. -· value: The exception instance, if an exception was raised. -· traceback: The traceback object, if an exception was raised. - -**Code Description**: The __exit__ method is a special method used in context management to define cleanup actions when exiting a context. In this specific implementation, the __exit__ method removes the last element from the USED_NODES list by calling the pop() method. This indicates that the context manager is maintaining a stack of nodes, and upon exiting the context, it ensures that the most recently added node is removed from the stack. This is a common pattern in resource management where resources are pushed onto a stack when entering a context and popped off when exiting to ensure proper cleanup and resource deallocation. - -**Note**: -- Ensure that the USED_NODES list is properly initialized and managed elsewhere in the code to avoid potential errors. -- This method does not handle exceptions; it simply performs the cleanup action. If exception handling is required, it should be implemented separately. -*** -## ClassDef FunModule -Doc is waiting to be generated... -### FunctionDef __init__(self, fun, description, n_outputs, node_dict, traceable_code, wrap_output, unpack_input, trainable, catch_execution_error, allow_external_dependencies, overwrite_python_recursion, ldict) -**__init__**: The function of __init__ is to initialize an instance of the FunModule class. - -**Parameters**: -- self: The instance of the FunModule class. -- fun: A callable object representing the function to be wrapped. -- description: An optional string describing the function module. -- n_outputs: An integer indicating the number of outputs of the function. -- node_dict: A dictionary, None, or "auto" representing the node dictionary. -- traceable_code: A boolean indicating whether the code is traceable or not. -- wrap_output: A boolean indicating whether to wrap the output or not. -- unpack_input: A boolean indicating whether to unpack the input or not. -- trainable: A boolean indicating whether the function is trainable or not. -- catch_execution_error: A boolean indicating whether to catch execution errors or not. -- allow_external_dependencies: A boolean indicating whether to allow external dependencies or not. -- overwrite_python_recursion: A boolean indicating whether to overwrite Python recursion or not. -- ldict: A dictionary or None representing the local dictionary. - -**Code Description**: The __init__ function initializes an instance of the FunModule class. It takes in various parameters such as fun, description, n_outputs, node_dict, traceable_code, wrap_output, unpack_input, trainable, catch_execution_error, allow_external_dependencies, overwrite_python_recursion, and ldict. - -The function starts by asserting that the ldict parameter is either None or a dictionary. If ldict is None, an empty dictionary is assigned to self.ldict. Otherwise, a copy of ldict is assigned to self.ldict. - -If traceable_code is True, the unpack_input parameter is set to False and the allow_external_dependencies parameter is set to True. This is because when the code is traceable, there is no need to unpack the input and there may be new nodes created in the code block. - -The function then asserts that the fun parameter is callable and that the node_dict parameter is either a dictionary, None, or "auto". - -Next, the source code of the function is obtained using the inspect.getsource() function. If the source code starts with a decorator line, the decorator line is removed and only the function definition is kept. Otherwise, the source code is trimmed. - -The function constructs an info dictionary containing information about the function module. This includes the function name, docstring, signature, source code, output, external dependencies, and node dictionary. - -If the description parameter is None, a description is generated using the function name and docstring. The get_op_name() function is called to extract the operator type from the description. The extracted operator type is combined with the function name and docstring to create a meaningful description. - -The function assigns the provided parameters to the corresponding attributes of the FunModule instance. It also sets the parameter attribute to None. - -If the n_outputs parameter is greater than 1, a warning message is displayed indicating that setting n_outputs>1 will be deprecated. - -Finally, if the trainable parameter is True, the function asserts that overwrite_python_recursion is also True. It then searches for the function signature in the source code and creates a ParameterNode object with the source code as the value and "__code" as the name. This ParameterNode represents the code constraint for the trainable function. - -**Note**: -- The ldict parameter must be a dictionary or None. -- The fun parameter must be a callable object. -- The node_dict parameter must be a dictionary, None, or "auto". -- The description parameter will be generated if it is None. -- The n_outputs parameter should be used with caution as setting n_outputs>1 will be deprecated. -- The trainable parameter requires overwrite_python_recursion to be True. -- The source code of the function is obtained using the inspect.getsource() function. -- The get_op_name() function is used to extract the operator type from the description. -- The info dictionary contains information about the function module. -- The parameter attribute is set to None unless the trainable parameter is True. -*** -### FunctionDef filter_global_namespaces(self, keys) -**filter_global_namespaces**: The function of filter_global_namespaces is to filter out keys that already exist in the current global namespace. - -**parameters**: The parameters of this Function. -· keys: A list of keys to be filtered. - -**Code Description**: The filter_global_namespaces function takes a list of keys as input and returns a new list containing only those keys that do not already exist in the current global namespace. The function initializes an empty list called filtered_keys to store the keys that pass the filtering criteria. It then iterates over each key in the input list. For each key, it checks if the key exists in the global namespace using the globals() function. If the key is found in the global namespace, it is skipped. Otherwise, the key is appended to the filtered_keys list. Finally, the function returns the filtered_keys list. - -**Note**: -- This function relies on the current global namespace, which means its behavior can vary depending on the existing global variables and functions at the time of execution. -- Ensure that the input list keys does not contain any unintended or sensitive keys that might be skipped due to their presence in the global namespace. - -**Output Example**: -If the global namespace contains the keys 'a' and 'b', and the input list is ['a', 'b', 'c', 'd'], the function will return ['c', 'd']. -*** -### FunctionDef fun(self) -**fun**: The function of fun is to execute dynamically generated code and return the resulting function. - -**parameters**: -- self: The instance of the class. -- *args: Variable length argument list. -- **kwargs: Arbitrary keyword arguments. - -**Code Description**: -The `fun` function is a method of the current class. It is responsible for executing dynamically generated code and returning the resulting function. The function takes in variable length arguments (`*args`) and arbitrary keyword arguments (`**kwargs`). - -The function first checks if the `parameter` attribute of the instance is `None`. If it is `None`, it returns the `_fun` attribute of the instance, which is the original function. - -If the `parameter` attribute is not `None`, the function retrieves the code from the `parameter` attribute and stores it in the `code` variable. It then tries to import all the global namespaces from the original function by creating a local dictionary (`ldict`) and copying the global dictionary (`gdict`) from the `_fun` attribute. The local dictionary is updated with the `ldict` attribute of the instance. The `exec` function is then called to define the function using the code, the global dictionary, and the local dictionary. The name of the function is extracted from the code using regular expression. The resulting function is stored in the `fun` variable. - -If there is an exception during the execution of the code (SyntaxError, NameError, KeyError, or OSError), an `ExecutionError` instance is created with details about the exception. The `ExecutionError` instance is then raised to indicate the error. - -Finally, the function returns the resulting function (`fun`). - -**Note**: -- The `fun` function is used within the `trace_nodes` context manager. -- The `fun` function relies on the `parameter` attribute to retrieve the dynamically generated code. -- The resulting function may be different from the original function if the code modifies the global namespaces. - -**Output Example**: -The output of the `fun` function is the resulting function that is executed from the dynamically generated code. -*** -### FunctionDef name(self) -**name**: The function of `name` is to retrieve the operator type from the description attribute of the FunModule instance. - -**parameters**: This method does not take any parameters other than `self`. - -**Code Description**: The `name` method is a member of the `FunModule` class in the `bundle.py` file. It is designed to extract and return the operator type from the `description` attribute of the `FunModule` instance. This is achieved by calling the `get_op_name` function, which processes the `description` string to find and return the operator type enclosed in square brackets at the beginning of the description. - -The `get_op_name` function uses a regular expression to search for the operator type. If the operator type is found, it is returned; otherwise, a `ValueError` is raised. This ensures that the `description` attribute of the `FunModule` instance is correctly formatted and contains the necessary operator type information. - -The `name` method is utilized within the `wrap` method of the same class. In the `wrap` method, the `name` method is used to set the `name` attribute of the `MessageNode` or `ExceptionNode` that is created based on the output of the function. This ensures that the nodes have a meaningful and accurate name that reflects the operator type. - -**Note**: -- The `description` attribute of the `FunModule` instance must contain the operator type enclosed in square brackets at the beginning. -- If the `description` does not contain the operator type, a `ValueError` will be raised by the `get_op_name` function. - -**Output Example**: -If the `description` attribute of the `FunModule` instance is "[Add] Add two numbers", the `name` method will return "Add". -*** -### FunctionDef forward(self) -**forward**: The `forward` function is responsible for executing the operator function (`self.fun`) and returning the resulting nodes. It takes in variable length arguments (`*args`) and arbitrary keyword arguments (`**kwargs`). - -**parameters**: -- `self`: The instance of the class. -- `*args`: Variable length argument list. -- `**kwargs`: Arbitrary keyword arguments. - -**Code Description**: -The `forward` function is a method of the `FunModule` class in the `bundle.py` file. It is the main function that executes the operator function and handles the processing of inputs and outputs. - -The function starts by initializing the `_args` and `_kwargs` variables with the provided arguments (`args` and `kwargs`). If the `unpack_input` attribute of the instance is `True`, the function extracts the data from the container of nodes by calling the `to_data` function on the arguments. - -Next, the function checks if the `overwrite_python_recursion` attribute is `True` and the `parameter` attribute is `None`. If both conditions are met, it sets the Python tracer to the `tracer` function defined within the `forward` function. This tracer modifies the local/global dictionary of the frame to ensure that recursive calls of the wrapped function call the unwrapped function. - -The function then enters a `trace_nodes` context manager using the `with` statement. This context manager tracks the nodes that are read or used in the operator function. The `used_nodes` set is created and appended to the global `USED_NODES` list. This set will contain the nodes that are accessed during the execution of the operator function. - -Within the context manager, the operator function (`self.fun`) is executed with the provided arguments (`_args` and `_kwargs`). If the `catch_execution_error` attribute is `True`, the function wraps the execution of the operator function in a try-except block. If an exception occurs during the execution, it is stored in the `outputs` variable. Otherwise, the `outputs` variable contains the result of the operator function. - -After the execution of the operator function, the context manager is exited, and the set of used nodes is available for further processing. - -The function then constructs the inputs of the `MessageNode` from the function inputs or the set of used nodes. If the `node_dict` attribute of the instance is `None`, the function generates a warning and creates a dictionary of inputs using the names of the nodes in the `used_nodes` set. If the `node_dict` attribute is not `None`, the function updates the input signature (`spec`) with the `node_dict` dictionary. It then iterates over the input signature and creates nodes for each input value using the `create_node` function. The resulting inputs dictionary is stored in the `inputs` variable. - -Next, the function identifies any external dependencies, which are nodes used to create the outputs but not included in the inputs. It creates a list of external dependencies by iterating over the `used_nodes` set and checking if each node is present in the `inputs` dictionary using the `contain` function. - -If the number of external dependencies is greater than 0 and the `allow_external_dependencies` attribute is `False`, the function raises a `TraceMissingInputsError` exception. This exception indicates that not all nodes used in the operator function are specified as inputs of the returned node. - -If the `GRAPH.TRACE` attribute is `False`, the `inputs` dictionary is cleared, as there is no need to keep track of the inputs if tracing is not enabled. - -Finally, the function wraps the output as a `MessageNode` or an `ExceptionNode` depending on the type of the output. If the `n_outputs` attribute of the instance is 1 or the output is an instance of `Exception`, the function calls the `wrap` function with the output, inputs, and external dependencies. Otherwise, it creates a tuple of wrapped nodes by calling the `wrap` function for each output element. - -The function returns the resulting nodes. - -**Note**: -- The `forward` function is the main function that executes the operator function and handles the processing of inputs and outputs. -- The `trace_nodes` context manager is used to track the nodes that are accessed during the execution of the operator function. -- The `tracer` function modifies the local/global dictionary of the frame to ensure that recursive calls of the wrapped function call the unwrapped function. -- The `to_data` function is used to extract the data from a node or a container of nodes. -- The `wrap` function is used to wrap the output of the operator function as a `MessageNode` or an `ExceptionNode`. -- The `TraceMissingInputsError` exception is raised when not all nodes used in the operator function are specified as inputs of the returned node. -- The `contain` function is used to check if a given node is present in a container of nodes. - -**Output Example**: -The `forward` function returns the resulting nodes of the operator function. The output can be a single `MessageNode` or `ExceptionNode` if the `n_outputs` attribute is 1 or the output is an exception. If the `n_outputs` attribute is greater than 1, the output is a tuple of `MessageNode` or `ExceptionNode` objects. -#### FunctionDef tracer(frame, event, arg) -**tracer**: The function of tracer is to modify the local and global dictionaries of a frame to ensure that recursive calls of a wrapped function invoke the unwrapped function. - -**parameters**: The parameters of this Function. -· frame: The frame object representing the current execution context. -· event: A string representing the type of event that occurred (e.g., 'call', 'return'). -· arg: An optional argument that may be passed to the tracer function (default is None). - -**Code Description**: The tracer function is designed to handle recursive calls within a wrapped function by modifying the local and global dictionaries of the frame. When the function is called, it first checks if the current frame's code object matches the code object of the wrapped function (`self._fun.__code__`). If it does, the function proceeds to handle different types of events: - -- **Call Event**: When the event is 'call', the function checks if the function name exists in the frame's local or global dictionaries. If the function name is found in the local dictionary and it does not match the wrapped function (`self._fun`), the `update_local` function is called to update the local variable to the wrapped function. If the function name is found in the global dictionary and it does not match the wrapped function, the original function (an instance of `FunModule`) is saved in `_bundled_func`, and the global dictionary is updated to point to the wrapped function. - -- **Return Event**: When the event is 'return', the function checks if the function name exists in the global dictionary. If it does, the global dictionary is restored to the original function saved in `_bundled_func`. - -The `update_local` function is used within the tracer to update the local variables in the frame. This ensures that recursive calls invoke the unwrapped function, maintaining the correct function behavior. - -**Note**: Points to note about the use of the code -- Ensure that the frame object passed to the tracer function is valid and corresponds to the correct execution context. -- Be cautious when modifying local and global variables in a frame, as it can affect the execution flow and state of the program. -- The tracer function relies on the `update_local` function to update local variables, which uses the `ctypes` module to interact with the Python C API. This may have implications for portability and compatibility across different Python versions and implementations. - -**Output Example**: The tracer function returns itself, allowing it to be used as a callback for tracing events. -*** -#### FunctionDef create_node(n) -**create_node**: The function of create_node is to convert an input into a Node object, specifically handling instances of FunModule by extracting their parameters if they exist. - -**parameters**: The parameters of this Function. -· n: The input to be converted into a Node. This can be an instance of FunModule or any other type that the node function can handle. - -**Code Description**: The create_node function is designed to facilitate the creation of Node objects from various inputs. It first checks if the input n is an instance of FunModule and whether it has a non-None parameter attribute. If both conditions are met, it assigns n to its parameter attribute. This step ensures that if n is a FunModule with a parameter, the parameter is used for the Node creation instead of the FunModule itself. After this check, the function calls the node function with n as its argument. The node function then processes n according to its own logic, which includes handling whether n is already a Node, and whether it should be trainable or have constraints. - -**Note**: -- This function is particularly useful when dealing with FunModule instances, as it ensures that their parameters are used for Node creation. -- The function relies on the node function to handle the actual creation of the Node object, including any additional parameters like name, trainable, and constraint. - -**Output Example**: A possible return value of the create_node function could be a Node object created from the parameter of a FunModule instance, or directly from the input if it is not a FunModule. For example, if n is a FunModule with a parameter, the return value would be a Node object created from that parameter. If n is a simple message, the return value would be a Node object created from that message. -*** -*** -### FunctionDef wrap(self, output, inputs, external_dependencies) -**wrap**: The function of wrap is to wrap the output as a MessageNode of inputs as the parents. - -**parameters**: -- output: The output of the operator function. -- inputs: The input nodes of the MessageNode. It can be a list or a dictionary. -- external_dependencies: A list of nodes that are used to create the outputs but not included in the inputs. - -**Code Description**: -The `wrap` function is a method of the `FunModule` class in the `bundle.py` file. It is designed to wrap the output of the operator function as a `MessageNode` with the specified inputs as its parents. The function takes three parameters: `output`, `inputs`, and `external_dependencies`. - -The `wrap` function first checks if the `wrap_output` attribute of the `FunModule` instance is `False`. If it is `False`, the function returns the output as is, assuming it is already a `Node` object. This is because there is no need to wrap the output if it is already a `Node`. - -If the `wrap_output` attribute is `True`, the function proceeds to check if the `parameter` attribute of the `FunModule` instance is not `None`. If it is not `None`, it means that the operator is a trainable operation and a new op eval needs to be created. In this case, the `inputs` dictionary is updated with the `__code` parameter, which is the code block of the function. The `description` and `name` variables are set accordingly to indicate that this is an eval operator. The `fun_name` attribute of the `FunModule` instance is also updated to "eval". - -If the `parameter` attribute is `None`, the `description` and `name` variables are set to the `description` and `name` attributes of the `FunModule` instance, respectively. - -Next, the function checks if the `output` is `None`. If it is `None`, it creates a `MessageNode` with `None` as the value and the specified `description`, `inputs`, `name`, and `info` attributes. This is useful when the operator does not produce any output. - -If the `output` is an instance of `Exception`, it creates an `ExceptionNode` with the `output` as the value and the specified `description`, `inputs`, `name`, and `info` attributes. The `ExceptionNode` represents an exception raised by the operator. - -If the `output` is neither `None` nor an instance of `Exception`, it creates a copy of the `info` attribute and updates it with the `output` value. It then creates a `MessageNode` with the `output` as the value and the specified `description`, `inputs`, `name`, and updated `info` attributes. - -The `wrap` function returns the created `MessageNode` or `ExceptionNode` depending on the type of the `output`. - -**Note**: -- The `wrap` function is used to wrap the output of the operator function as a `MessageNode` or `ExceptionNode`. -- The `wrap_output` attribute of the `FunModule` instance determines whether the output needs to be wrapped. -- The `parameter` attribute of the `FunModule` instance determines whether the operator is a trainable operation. -- The `description`, `name`, and `info` attributes of the `FunModule` instance are used to provide additional information for the created nodes. - -**Output Example**: -If the `output` is `None`, the function returns a `MessageNode` with `None` as the value: -``` -MessageNode(None, description="[Node] This is a node in a computational graph.", inputs=inputs, name=name, info=info) -``` -If the `output` is an exception, the function raises an `ExecutionError` with an `ExceptionNode` containing the exception details. -*** -### FunctionDef is_valid_output(output) -**is_valid_output**: The function of is_valid_output is to check whether the given output is a valid output for a computational graph node. - -**parameters**: -- output: The output to be checked. - -**Code Description**: -The `is_valid_output` function takes an `output` as input and checks whether it is a valid output for a computational graph node. The function returns `True` if the `output` is an instance of the `Node` class or if it is a tuple containing only instances of the `Node` class. Otherwise, it returns `False`. - -The function first checks if the `output` is an instance of the `Node` class using the `isinstance` function. If it is, the function returns `True`. - -If the `output` is not an instance of the `Node` class, the function checks if it is a tuple using the `isinstance` function. If it is a tuple, the function uses a list comprehension and the `isinstance` function to check if all elements in the tuple are instances of the `Node` class. If all elements are instances of the `Node` class, the function returns `True`. Otherwise, it returns `False`. - -**Note**: -- The `is_valid_output` function is used to validate the output of a computational graph node. It ensures that the output is compatible with the expected input types for further computations. -- The function assumes that the `Node` class is defined and imported correctly. - -**Output Example**: -- Example 1: - ```python - output = Node(5) - print(is_valid_output(output)) - ``` - Output: - ``` - True - ``` - -- Example 2: - ```python - output = (Node(1), Node(2), Node(3)) - print(is_valid_output(output)) - ``` - Output: - ``` - True - ``` - -- Example 3: - ```python - output = (Node(1), 2, Node(3)) - print(is_valid_output(output)) - ``` - Output: - ``` - False - ``` -*** -### FunctionDef __get__(self, obj, objtype) -**__get__**: The function of __get__ is to support instance methods by binding the __call__ method to an instance of the Module class. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the FunModule class. -· obj: The instance of the class where the FunModule instance is accessed as an attribute. -· objtype: The type of the class where the FunModule instance is accessed as an attribute. - -**Code Description**: The __get__ method is a descriptor method used to support instance methods in the FunModule class. When an instance of FunModule is accessed as an attribute of another class instance, the __get__ method is invoked. This method uses functools.partial to bind the __call__ method of the FunModule instance to the obj parameter, which is the instance of the class where FunModule is accessed. - -By doing this, the __call__ method of the FunModule instance is effectively converted into an instance method of the obj instance. This allows the __call__ method to be invoked with obj as its first argument, enabling it to operate in the context of the obj instance. - -In the context of the project, the __call__ method of the FunModule class is designed to invoke the forward method of the Module class with the provided arguments. The __get__ method ensures that when the __call__ method is accessed through an instance of another class, it behaves as an instance method, maintaining the correct binding to the obj instance. - -**Note**: -- The __get__ method is crucial for enabling the FunModule class to be used as a descriptor, allowing its __call__ method to be bound to instances of other classes. -- Ensure that the obj parameter is an instance of a class that correctly utilizes the FunModule instance as an attribute. - -**Output Example**: The return value of the __get__ method is a functools.partial object that binds the __call__ method to the obj instance. This allows the __call__ method to be invoked as if it were an instance method of the obj instance. For example, if obj is an instance of a class that has a FunModule instance as an attribute, accessing this attribute and calling it will invoke the __call__ method with obj as its first argument. -*** -## FunctionDef to_data(obj) -**to_data**: The function of to_data is to extract the data from a node or a container of nodes. - -**parameters**: -- obj: The input object, which can be a node or a container of nodes. - -**Code Description**: -The to_data function is designed to extract the data from a node or a container of nodes. It takes an input object and recursively extracts the data from each node in the object. The function handles different types of objects and performs specific operations based on their type. - -For node containers (tuple, list, dict, set, NodeContainer), the function recursively extracts the data from each node in the container. It uses list comprehension or dictionary comprehension to iterate over the nodes and call the to_data function recursively on each node. - -For individual nodes (instances of the Node class), the function simply returns the data attribute of the node. - -If the input object is an instance of NodeContainer, the function creates a copy of the object and iterates over its attributes using the __dict__ attribute. It then sets the corresponding attribute in the output object to the result of calling the to_data function recursively on the attribute value. - -If the input object is not a node or a node container, the function simply returns the object as is. - -**Note**: -- The function relies on the isinstance() function to determine the type of the input object and perform the appropriate operations. -- The function uses the copy module to create a copy of the NodeContainer object. -- The function assumes that the Node and NodeContainer classes are defined and imported correctly. - -**Output Example**: -- Input: Node(5) - Output: 5 - -- Input: [Node(1), Node(2), Node(3)] - Output: [1, 2, 3] - -- Input: {Node(1): Node(2), Node(3): Node(4)} - Output: {1: 2, 3: 4} -## FunctionDef update_local(frame, name, value) -**update_local**: The function of update_local is to update the value of a local variable in a given frame. - -**parameters**: The parameters of this Function. -· frame: The frame object where the local variable resides. -· name: The name of the local variable to be updated. -· value: The new value to be assigned to the local variable. - -**Code Description**: The update_local function is designed to modify the value of a local variable within a specific frame. It takes three parameters: the frame object, the name of the local variable, and the new value to be assigned to that variable. The function first updates the local variable in the frame's f_locals dictionary. Then, it calls the PyFrame_LocalsToFast function from the ctypes.pythonapi module to ensure that the changes are reflected in the frame's fast locals array, which is used by the Python interpreter for efficient variable access. - -In the context of its usage within the project, update_local is called by the tracer function in the FunModule class's forward method. The tracer function is responsible for modifying the local and global dictionaries of a frame to handle recursive calls of a wrapped function. Specifically, update_local is used to replace the current function in the frame's local variables with the original function when a recursive call is detected. This ensures that the recursive call invokes the unwrapped function rather than the bundled function, maintaining the correct function behavior. - -**Note**: Points to note about the use of the code -- Ensure that the frame object passed to update_local is valid and corresponds to the correct execution context. -- Be cautious when modifying local variables in a frame, as it can affect the execution flow and state of the program. -- The ctypes module is used to interact with the Python C API, which may have implications for portability and compatibility across different Python versions and implementations. -## FunctionDef test(x) -**test**: The function of test is to concatenate the string " world" to the data attribute of the input object. - -**parameters**: The parameters of this Function. -· x: An object that must have a data attribute containing a string. - -**Code Description**: The test function takes a single parameter, x, which is expected to be an object with a data attribute. The function accesses the data attribute of the input object and concatenates the string " world" to it. The result of this concatenation is then returned as the output of the function. - -**Note**: -- Ensure that the input object x has a data attribute that is a string; otherwise, the function will raise an AttributeError or TypeError. -- This function does not perform any type checking or error handling, so it is crucial to pass an appropriate object to avoid runtime errors. - -**Output Example**: -If the input object x has a data attribute with the value "Hello", the function will return "Hello world". diff --git a/generated_docs/opto/trace/containers.md b/generated_docs/opto/trace/containers.md deleted file mode 100644 index 2a7cae0b..00000000 --- a/generated_docs/opto/trace/containers.md +++ /dev/null @@ -1,386 +0,0 @@ -## ClassDef SeqIterable -**SeqIterable**: The function of SeqIterable is to provide an iterable interface for a wrapped list-like object, allowing it to be iterated over in a sequential manner. - -**attributes**: The attributes of this Class. -· _index: An integer that keeps track of the current position in the iteration. -· wrapped_list: The list-like object that is being wrapped and iterated over. - -**Code Description**: The SeqIterable class is designed to wrap a list-like object and provide an iterator interface for it. This allows the wrapped object to be iterated over using Python's iterator protocol. - -- The `__init__` method initializes the SeqIterable object with a wrapped list-like object and sets the initial index to 0. -- The `__iter__` method resets the index to 0 and returns the SeqIterable object itself as an iterator. -- The `__next__` method retrieves the next item from the wrapped list. If the end of the list is reached, it raises a StopIteration exception to signal the end of the iteration. Each item retrieved is wrapped in a node object, and if the wrapped list is not already a parent of the node, it is added as a parent. - -The SeqIterable class is utilized in the `iterate` function, which determines the appropriate iterable class to use based on the type of the input object. If the input is a list or tuple, it is wrapped in a SeqIterable object. If the input is a set, it is first converted to a list and then wrapped in a SeqIterable object. This ensures that various collection types can be iterated over in a consistent manner. - -**Note**: -- The wrapped list-like object must have a `data` attribute that is a list or tuple. -- The node function is used to wrap each item in the list, and it is assumed that this function and the Node class are defined elsewhere in the codebase. -- The wrapped list-like object must support being checked for membership in the parents attribute of a node. - -**Output Example**: -If the wrapped list contains the elements [1, 2, 3], iterating over the SeqIterable object would yield: -``` -node(1) -node(2) -node(3) -``` -Each element is wrapped in a node object before being returned. -### FunctionDef __init__(self, wrapped_list) -**__init__**: The function of __init__ is to initialize an instance of the SeqIterable class with a given list. - -**parameters**: The parameters of this Function. -· wrapped_list: A list that will be wrapped by the SeqIterable instance. - -**Code Description**: The __init__ method is a constructor that initializes an instance of the SeqIterable class. It takes one parameter, `wrapped_list`, which is expected to be a list. Inside the method, two instance variables are set: -- `self._index`: This is initialized to 0 and will likely be used to keep track of the current position in the iteration process. -- `self.wrapped_list`: This is assigned the value of the `wrapped_list` parameter, effectively storing the provided list within the instance for further operations. - -**Note**: Ensure that the `wrapped_list` parameter passed to the __init__ method is a list, as the class is designed to work with list-like structures. -*** -### FunctionDef __iter__(self) -**__iter__**: The function of __iter__ is to initialize the iteration process for the SeqIterable object and return the iterator itself. - -**parameters**: The parameters of this Function. -· This function does not take any parameters other than the implicit 'self' which refers to the instance of the SeqIterable class. - -**Code Description**: The __iter__ method is a special method in Python that is used to make an object iterable. When this method is called, it sets the internal index (_index) of the SeqIterable object to 0. This index is used to keep track of the current position during iteration. After initializing the index, the method returns the instance of the SeqIterable object itself, which will be used as the iterator. This allows the object to be used in iteration contexts such as loops. - -**Note**: -- Ensure that the SeqIterable class has a properly defined __next__ method to work in conjunction with __iter__ for full iterator functionality. -- The __iter__ method should be called before starting the iteration process to reset the index. - -**Output Example**: -When the __iter__ method is called on an instance of SeqIterable, it returns the instance itself. For example: - -```python -seq_iterable = SeqIterable() -iterator = iter(seq_iterable) -print(iterator is seq_iterable) # Output: True -``` - -In this example, calling iter(seq_iterable) invokes the __iter__ method, which returns the seq_iterable instance itself, confirming that the object is ready for iteration. -*** -### FunctionDef __next__(self) -**__next__**: The function of __next__ is to iterate over the wrapped list of nodes and return the next node in the sequence. - -**parameters**: -- self: Refers to the instance of the SeqIterable class that contains this method. - -**Code Description**: -The __next__ function is an implementation of the iterator protocol for the SeqIterable class. It allows users to iterate over the wrapped list of nodes and retrieve the next node in the sequence. - -The function first checks if the current index (_index) is less than the length of the wrapped list of nodes. If it is, it retrieves the node at the current index using the wrapped_list attribute and assigns it to the result variable. It then increments the index by 1 to prepare for the next iteration. - -Next, the function creates a node object from the result using the node function from opto.trace.nodes. This step ensures that the result is always a valid node object, even if it was already a node or a different type of object. - -After creating the node object, the function checks if the wrapped_list is not already a parent of the result node. If it is not, it adds the wrapped_list as a parent of the result node using the _add_parent method from opto.trace.nodes. This step ensures that the hierarchical structure of the graph is maintained correctly. - -Finally, if the current index is equal to or greater than the length of the wrapped list, the function raises a StopIteration exception. This signals the end of the iteration and is the expected behavior for iterators. - -The __next__ function is typically used in a loop or with the next() function to iterate over the nodes in a SeqIterable object. For example: - -```python -seq_iterable = SeqIterable(wrapped_list) -for node in seq_iterable: - # Do something with each node -``` - -**Note**: -- The __next__ function is part of the iterator protocol and is automatically called when iterating over a SeqIterable object. -- The wrapped_list attribute should be a list-like object that supports indexing and has a length. -- The function relies on the node function from opto.trace.nodes to create node objects from the elements of the wrapped list. -- The _add_parent method from opto.trace.nodes is used to maintain the hierarchical structure of the graph. -- The function raises a StopIteration exception when there are no more nodes to iterate over. - -**Output Example**: A possible return value of the __next__ function could be a node object representing the next node in the sequence. -*** -## FunctionDef to_list_implicit(x) -**to_list_implicit**: The function of to_list_implicit is to convert any given iterable into a list. - -**parameters**: The parameters of this Function. -· x: An iterable object of any type (e.g., set, tuple, etc.) - -**Code Description**: The to_list_implicit function takes a single parameter, x, which is expected to be an iterable. The function converts this iterable into a list using Python's built-in list() constructor and returns the resulting list. This conversion is implicit, meaning it does not check the type of the input explicitly but relies on the list() constructor to handle the conversion. - -In the context of its usage within the project, to_list_implicit is called by the iterate function. The iterate function is designed to handle various types of data structures, including Node objects, lists, tuples, sets, and dictionaries. When iterate encounters a set, it uses to_list_implicit to convert the set into a list. This conversion is necessary because the subsequent processing within iterate, specifically the creation of a SeqIterable object, requires a list rather than a set. - -**Note**: -- The input to to_list_implicit must be an iterable; otherwise, the list() constructor will raise a TypeError. -- This function does not perform any type checking or validation on the input. - -**Output Example**: -If the input is a set {1, 2, 3}, the function will return [1, 2, 3]. -If the input is a tuple (4, 5, 6), the function will return [4, 5, 6]. -## FunctionDef iterate(x) -**iterate**: The function of iterate is to provide an iterable interface for different types of objects, allowing them to be iterated over in a consistent manner. - -**parameters**: -- x: The input object to be iterated over. - -**Code Description**: The iterate function is designed to handle various types of objects and determine the appropriate iterable class to use based on the type of the input object. It follows a series of conditional statements to check the type of the input object and returns the corresponding iterable object. - -- If the input object is a subclass of the Node class, it checks the type of the data attribute of the object. If the data attribute is a list or tuple, it creates a SeqIterable object and returns it. If the data attribute is a set, it converts the set to a list using the to_list_implicit function and then creates a SeqIterable object with the converted list. If the data attribute is a dictionary, it creates a DictIterable object and returns it. If the data attribute is of any other type, it raises an exception indicating that the object cannot be iterated over. - -- If the input object is a list or tuple, it creates a SeqIterable object with the input object and returns it. - -- If the input object is a set, it converts the set to a list using the to_list_implicit function and then creates a SeqIterable object with the converted list. - -- If the input object is a dictionary, it creates a DictIterable object with the input object and returns it. - -- If the input object is of any other type, it raises an exception indicating that the object cannot be iterated over. - -The iterate function utilizes the SeqIterable and DictIterable classes defined in the code to provide the iterable interface for different types of objects. It ensures that objects of various collection types can be iterated over in a consistent manner. - -**Note**: -- The input object must have a data attribute that is a list, tuple, set, or dictionary. -- The to_list_implicit function is used to convert a set to a list. -- The node function is used to wrap each item in the list or dictionary with a node object. -- The Node class is assumed to be defined elsewhere in the codebase. - -**Output Example**: -If the input object is a list [1, 2, 3], iterating over the returned SeqIterable object would yield: -``` -node(1) -node(2) -node(3) -``` -If the input object is a dictionary {'a': 1, 'b': 2}, iterating over the returned DictIterable object would yield: -``` -(node('a'), 1) -(node('b'), 2) -``` -## ClassDef DictIterable -**DictIterable**: The function of DictIterable is to provide an iterable interface for dictionary-like objects, allowing iteration over key-value pairs. - -**attributes**: The attributes of this Class. -· _index: An integer that keeps track of the current position in the iteration. -· wrapped_dict: The dictionary-like object that is being wrapped and iterated over. -· keys: A list of keys from the wrapped_dict, used to facilitate iteration. - -**Code Description**: The DictIterable class is designed to enable iteration over dictionary-like objects. When an instance of DictIterable is created, it takes a dictionary-like object (wrapped_dict) as an argument. The constructor initializes the _index attribute to 0, stores the wrapped_dict, and extracts the keys from the wrapped_dict's data attribute, storing them in the keys attribute. - -The __iter__ method resets the _index to 0 and returns the instance itself, making the object an iterator. - -The __next__ method is responsible for returning the next item in the iteration. It checks if the current _index is less than the length of the keys list. If so, it retrieves the key at the current index, constructs a tuple containing a node object created from the key and the corresponding value from the wrapped_dict, and increments the _index. Before returning the tuple, it adds the wrapped_dict as a parent to both the key and value nodes. If the _index exceeds the length of the keys list, a StopIteration exception is raised to signal the end of the iteration. - -The DictIterable class is utilized in the iterate and items functions. The iterate function determines the type of the input object and returns an appropriate iterable object. If the input is a dictionary or a dictionary-like object, iterate returns an instance of DictIterable. Similarly, the items function checks if the input object's data attribute is a dictionary and returns a DictIterable instance if true. - -**Note**: -- The wrapped_dict parameter must be a dictionary-like object with a data attribute that is a dictionary. -- The node function and the _add_parent method must be defined elsewhere in the codebase for DictIterable to function correctly. - -**Output Example**: -Assuming the wrapped_dict contains {'a': 1, 'b': 2}, iterating over an instance of DictIterable would yield: -(node('a'), 1) -(node('b'), 2) -### FunctionDef __init__(self, wrapped_dict) -**__init__**: The function of __init__ is to initialize an instance of the DictIterable class with a given dictionary. - -**parameters**: The parameters of this Function. -· wrapped_dict: A dictionary-like object that contains the data to be wrapped by the DictIterable instance. - -**Code Description**: The __init__ method initializes an instance of the DictIterable class. It takes one parameter, `wrapped_dict`, which is expected to be a dictionary-like object. Inside the method, the instance variable `_index` is initialized to 0, which will likely be used to keep track of the current position during iteration. The `wrapped_dict` parameter is assigned to the instance variable `wrapped_dict`, allowing the instance to store and access the provided dictionary. Additionally, the keys of the dictionary are extracted and converted into a list, which is then assigned to the instance variable `keys`. This list of keys will be used for iterating over the dictionary. - -**Note**: Ensure that the `wrapped_dict` parameter passed to the __init__ method is a dictionary-like object with a `data` attribute that contains the actual dictionary. This is crucial for the proper functioning of the DictIterable class. -*** -### FunctionDef __iter__(self) -**__iter__**: The function of __iter__ is to initialize the iteration process for the DictIterable object. - -**parameters**: This function does not take any parameters. - -**Code Description**: The __iter__ method is a special method in Python that is used to make an object iterable. When this method is called, it sets the internal index `_index` of the object to 0. This index is likely used to keep track of the current position during iteration. After initializing the index, the method returns the object itself (`self`). This allows the object to be used in iteration contexts, such as in a for loop. By implementing the __iter__ method, the DictIterable object conforms to the iterator protocol, which requires an __iter__ method that returns the iterator object itself. - -**Note**: -- Ensure that the DictIterable class has a corresponding __next__ method to complete the iterator protocol. The __next__ method should define how the iteration progresses and when it stops. -- The __iter__ method should not modify the underlying data structure of the object; it should only initialize the state required for iteration. - -**Output Example**: -When the __iter__ method is called on a DictIterable object, it does not produce a direct output but prepares the object for iteration. For example: - -```python -dict_iterable = DictIterable() -iterator = iter(dict_iterable) -``` - -In this example, `iterator` is the same as `dict_iterable`, now ready to be used in a loop or any other iteration context. -*** -### FunctionDef __next__(self) -**__next__**: The function of __next__ is to iterate over the items in the wrapped dictionary, returning each key-value pair as a tuple of Node objects. - -**parameters**: The parameters of this Function. -- This function does not take any parameters. - -**Code Description**: The __next__ method is designed to facilitate iteration over a dictionary wrapped within the DictIterable object. It maintains an internal index (_index) to keep track of the current position in the iteration. The method first checks if the current index is less than the length of the keys in the dictionary. If so, it retrieves the key at the current index and constructs a tuple (result) consisting of two elements: -1. A Node object created from the key. -2. A Node object created from the corresponding value in the wrapped dictionary. - -Both elements of the tuple are created using the node function, which ensures that they are properly instantiated as Node objects. After creating the tuple, the method increments the internal index (_index) by one to move to the next item in the subsequent call. - -Additionally, the method calls the _add_parent method on both elements of the tuple, passing the wrapped dictionary as the parent. This establishes a parent-child relationship between the nodes and the dictionary, which can be useful for tracking dependencies or maintaining hierarchical structures. - -If the current index is equal to or greater than the length of the keys, the method raises a StopIteration exception, signaling that the iteration is complete. - -**Note**: -- The __next__ method is intended to be used in conjunction with an iterator protocol, typically within a for loop or similar construct. -- The method relies on the node function to create Node objects, ensuring consistency and proper initialization. -- The _add_parent method is called on both the key and value nodes to establish a parent-child relationship with the wrapped dictionary. - -**Output Example**: A possible return value of the __next__ method could be: -``` -(node('some_key'), node('some_value')) -``` -where 'some_key' and 'some_value' are entries in the wrapped dictionary, and both are converted to Node objects. -*** -## FunctionDef items(x) -**items**: The function of items is to return an iterable interface for dictionary-like objects, allowing iteration over key-value pairs if the input object's data attribute is a dictionary. - -**parameters**: The parameters of this Function. -· x: An object that is expected to have a data attribute. - -**Code Description**: The items function is designed to facilitate iteration over the key-value pairs of an object's data attribute, provided that this attribute is a dictionary. The function first checks if the data attribute of the input object x is of type dict. If it is not, the function returns an AttributeError, indicating that items cannot be retrieved from the given type. If the data attribute is indeed a dictionary, the function returns an instance of DictIterable, which is a class designed to enable iteration over dictionary-like objects. - -The DictIterable class, when instantiated, takes the dictionary-like object (wrapped_dict) and provides an iterable interface. It initializes an index to keep track of the current position in the iteration and extracts the keys from the wrapped_dict's data attribute. The __iter__ method resets the index and returns the instance itself, making it an iterator. The __next__ method retrieves the next item in the iteration, constructs a tuple containing a node object created from the key and the corresponding value from the wrapped_dict, and increments the index. If the index exceeds the length of the keys list, a StopIteration exception is raised to signal the end of the iteration. - -**Note**: -- The input object x must have a data attribute that is a dictionary for the function to work correctly. -- The node function and the _add_parent method must be defined elsewhere in the codebase for DictIterable to function correctly. - -**Output Example**: -Assuming the input object's data attribute contains {'a': 1, 'b': 2}, calling the items function would yield: -(node('a'), 1) -(node('b'), 2) -## ClassDef Seq -**Seq**: The function of Seq is to represent a sequence with a defined length and index, converting Python's list or tuple into a Seq object. - -**attributes**: The attributes of this Class. -· data: Inherited from UserList, it stores the sequence data. - -**Code Description**: The Seq class is a specialized container that inherits from both UserList and ParameterContainer. It is designed to handle sequences, converting Python lists or tuples into Seq objects. The class provides a method to retrieve a dictionary of parameters contained within the sequence. - -The `__init__` method initializes the Seq object. It accepts a variable number of arguments (`*args`). If a single argument is passed and it has both `__len__` and `__getitem__` attributes (indicating it is a sequence), it is used directly as the sequence. Otherwise, the arguments are treated as individual elements of the sequence. The superclass initializer is then called with the sequence. - -The `parameters_dict` method returns a dictionary of all parameters in the model, including both trainable and non-trainable parameters. It iterates over the elements in the sequence (`self.data`). If an element is an instance of ParameterNode, it adds it to the dictionary with its name as the key. If an element is an instance of ParameterContainer, it adds it to the dictionary with the string representation of the container as the key. The method ensures that all values in the dictionary are instances of either ParameterNode or ParameterContainer. - -The Seq class leverages the functionality of the ParameterContainer class, which serves as a container for parameter nodes. The ParameterContainer class provides methods to retrieve a flattened list of parameters and a dictionary of all parameters in the model. The Seq class uses the `parameters_dict` method to gather parameters from its elements, ensuring they are correctly identified and stored. - -**Note**: -- The Seq class is designed to work seamlessly with Python's list and tuple types, converting them into Seq objects. -- When using the Seq class, ensure that the elements within the sequence are either ParameterNode or ParameterContainer instances to maintain the integrity of the `parameters_dict` method. - -**Output Example**: -```python -{ - 'param1': , - 'param2': , - 'container1': , - 'container2': -} -``` -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the Seq class. - -**parameters**: The parameters of this Function. -· *args: A variable-length argument list that can contain one or more elements. - -**Code Description**: The __init__ method is designed to initialize an instance of the Seq class. It first checks if there is exactly one argument passed and if this argument has both the `__len__` and `__getitem__` attributes, which are typical of sequence-like objects (e.g., lists, tuples). If these conditions are met, the single argument is treated as a sequence and assigned to the variable `seq`. If the conditions are not met, all arguments are treated as individual elements and are collectively assigned to `seq` as a tuple. Finally, the method calls the `__init__` method of the superclass with `initlist=seq`, passing the sequence or tuple to the superclass for further initialization. - -**Note**: -- Ensure that if a single argument is passed, it should be a sequence-like object (having `__len__` and `__getitem__` attributes) to be treated as such. -- If multiple arguments are passed, they will be treated as individual elements and combined into a tuple. -- This method leverages the flexibility of accepting both single sequence-like objects and multiple individual elements, making it versatile for different initialization scenarios. -*** -### FunctionDef parameters_dict(self) -**parameters_dict**: The function of parameters_dict is to return a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. - -**parameters**: -- No parameters are defined within the provided code snippet. - -**Code Description**: -The `parameters_dict` method is used to retrieve a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. It iterates over the items in the `self.data` attribute, which is assumed to be a dictionary-like object. For each item, it checks if the value is an instance of `ParameterNode`. If it is, it adds the value to the `parameters` dictionary with the attribute name as the key. If the value is an instance of `ParameterContainer`, it adds the value to the `parameters` dictionary with the attribute name as the key. - -The `parameters_dict` method ensures that all the values in the `parameters` dictionary are instances of `ParameterNode` or `ParameterContainer` by asserting that the `isinstance` condition holds true for all values. - -The `parameters_dict` method is called internally by the `parameters` method to retrieve the parameters dictionary. - -**Note**: -- The `parameters_dict` method assumes that the `self.data` attribute is a dictionary-like object containing the parameters. -- The `parameters_dict` method does not specify the name of the container when adding a `ParameterContainer` to the `parameters` dictionary. This could be a potential improvement to consider. - -**Output Example**: -```python -{ - 'param1': , - 'param2': , - 'container1': , - 'container2': -} -``` -*** -## ClassDef Map -**Map**: The function of Map is to serve as a specialized container that maps keys to values, converting Python's standard dictionary into a Map object. - -**attributes**: The attributes of this Class. -· No specific attributes are defined within the provided code snippet. - -**Code Description**: -The `Map` class is a specialized container that inherits from both `UserDict` and `ParameterContainer`. It is designed to map keys to values, similar to a Python dictionary, but with additional functionality specific to handling parameters in a model. - -- **Initialization**: The `__init__` method initializes the `Map` object by calling the constructor of its parent classes with the provided `mapping`. This ensures that the `Map` object is initialized with the given key-value pairs. - -- **parameters_dict Method**: The `parameters_dict` method returns a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. The dictionary contains `ParameterNode` or `ParameterContainer` objects. The method iterates over the items in the `data` attribute (inherited from `UserDict`), checking the type of each key and value: - - If the value is an instance of `ParameterNode`, it is added to the `parameters` dictionary. - - If the value is an instance of `ParameterContainer`, it is also added to the `parameters` dictionary, but the key is converted to a string representation. - - If the key is an instance of `ParameterNode`, it is added to the `parameters` dictionary with its string representation as the key. - - If the key is an instance of `ParameterContainer`, an exception is raised because a `Map` cannot have a container as a key. - -The method asserts that all values in the `parameters` dictionary are instances of either `ParameterNode` or `ParameterContainer` before returning the dictionary. - -**Note**: -- The `Map` class ensures that all keys and values adhere to specific types (`ParameterNode` or `ParameterContainer`), maintaining the integrity of the parameter mapping. -- The `parameters_dict` method is crucial for retrieving a structured dictionary of parameters, which is essential for model optimization and parameter management. -- The `Map` class cannot have a `ParameterContainer` as a key, which is enforced by raising an exception. - -**Output Example**: -```python -{ - 'param1': , - 'param2': , - 'container1': -} -``` -### FunctionDef __init__(self, mapping) -**__init__**: The function of __init__ is to initialize an instance of the Map class with a given mapping. - -**parameters**: The parameters of this Function. -· mapping: A dictionary or any other mapping object that will be used to initialize the Map instance. - -**Code Description**: The __init__ method is a constructor for the Map class. It takes a single parameter, `mapping`, which is expected to be a dictionary or another type of mapping object. The method then calls the `__init__` method of its superclass with the provided `mapping` as an argument. This ensures that the Map instance is properly initialized with the given mapping data. The use of `super().__init__(mapping)` indicates that the Map class is likely inheriting from a parent class that requires initialization with a mapping object. - -**Note**: Ensure that the `mapping` parameter passed to the __init__ method is a valid mapping object, such as a dictionary, to avoid any initialization errors. -*** -### FunctionDef parameters_dict(self) -**parameters_dict**: The function of parameters_dict is to return a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. - -**parameters**: -- self: The current object. - -**Code Description**: -The `parameters_dict` method is used to retrieve a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. It iterates over the items in the `data` attribute of the current object and checks the type of each value. If the value is an instance of `ParameterNode`, it adds it to the `parameters` dictionary with the key as the corresponding key in the `data` attribute. If the value is an instance of `ParameterContainer`, it adds it to the `parameters` dictionary with the key as the string representation of the container. - -Additionally, the method checks the type of each key in the `data` attribute. If the key is an instance of `ParameterNode`, it adds it to the `parameters` dictionary with the key as the string representation of the node. If the key is an instance of `ParameterContainer`, it raises an exception since the key of a Map cannot be a container. - -Finally, the method asserts that all the values in the `parameters` dictionary are instances of `ParameterNode` or `ParameterContainer` and returns the `parameters` dictionary. - -**Note**: -- The `parameters_dict` method is called internally by the `parameters` method to retrieve the parameters dictionary. -- The `parameters_dict` method includes both trainable and non-trainable parameters in the returned dictionary. - -**Output Example**: -{ - 'param1': , - 'param2': , - 'container1': , - 'container2': -} -*** diff --git a/generated_docs/opto/trace/errors.md b/generated_docs/opto/trace/errors.md deleted file mode 100644 index d2f7ed34..00000000 --- a/generated_docs/opto/trace/errors.md +++ /dev/null @@ -1,112 +0,0 @@ -## ClassDef ExecutionError -**ExecutionError**: The function of ExecutionError is to serve as a base class for handling execution errors in code tracing. - -**attributes**: The attributes of this Class. -· exception_node: An instance of ExceptionNode that contains details about the exception. - -**Code Description**: The ExecutionError class is designed to encapsulate errors that occur during the execution of code within a tracing context. It inherits from the built-in Exception class, providing additional context through the exception_node attribute. - -- The `__init__` method initializes the ExecutionError instance with an ExceptionNode object, which contains detailed information about the exception, including the error message, inputs, and other metadata. The base Exception class is then initialized with the data from the exception_node. - -- The `__str__` method provides a string representation of the ExecutionError, which includes the data from the exception_node. This makes it easier to understand the nature of the error when it is printed or logged. - -In the project, ExecutionError is used in the following contexts: - -1. **opto\trace\bundle.py/FunModule/fun**: Within the `fun` method, ExecutionError is raised when there is a SyntaxError, NameError, KeyError, or OSError during the execution of dynamically generated code. The ExceptionNode is created with details about the error and passed to ExecutionError, which is then raised to signal the issue. - -2. **opto\trace\bundle.py/FunModule/wrap**: In the `wrap` method, ExecutionError is raised if the output of a function is an exception. An ExceptionNode is created with the exception details and passed to ExecutionError, which is then raised to indicate the error. - -3. **opto\trace\nodes.py/ExceptionNode/__init__**: The ExceptionNode class's `__init__` method checks if the value is an instance of ExecutionError. If not, it formats the exception message accordingly. This ensures that ExecutionError instances are handled correctly within the ExceptionNode. - -**Note**: When using ExecutionError, ensure that the exception_node provided contains all necessary information about the error, as this will be used to initialize the base Exception class and provide a meaningful error message. - -**Output Example**: -If an ExecutionError is raised due to a SyntaxError in the dynamically executed code, the string representation might look like: -``` -ExecutionError: (SyntaxError) invalid syntax (, line 1) -``` -This output indicates that a SyntaxError occurred, providing the specific error message and location. -### FunctionDef __init__(self, exception_node) -**__init__**: The function of __init__ is to initialize an instance of the ExecutionError class with a given ExceptionNode. - -**parameters**: The parameters of this Function. -· exception_node: An instance of ExceptionNode that contains the exception message and related data. - -**Code Description**: The __init__ method of the ExecutionError class is responsible for initializing an instance of the class. It takes one parameter, exception_node, which is an instance of ExceptionNode. This ExceptionNode contains the exception message and related data. - -Upon initialization, the method assigns the provided exception_node to the instance variable self.exception_node. It then calls the __init__ method of its superclass with the data retrieved from the exception_node. This is achieved by accessing the data attribute of the exception_node, which returns the internal data of the node. The superclass's __init__ method is thus provided with this data, ensuring that the ExecutionError instance is properly initialized with the relevant exception information. - -The relationship with its callees in the project is as follows: -- The data method of the ExceptionNode class is called to retrieve the internal data of the node. This data is then passed to the superclass's __init__ method to complete the initialization process. - -**Note**: It is important to ensure that the exception_node parameter is a valid instance of ExceptionNode, as the method relies on the data attribute of this object to function correctly. If the exception_node does not have the expected structure, the initialization process may fail. -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to provide a string representation of the ExecutionError object, specifically detailing the error message associated with the exception node. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the ExecutionError class. - -**Code Description**: The __str__ method is designed to return a formatted string that represents the ExecutionError instance. It accesses the `exception_node` attribute of the ExecutionError object and retrieves its data using the `data` method. The `data` method, defined in the AbstractNode class, returns the internal data of the node, which in this context is the error message or relevant data associated with the exception. The __str__ method then formats this data into a string prefixed with "ExecutionError: ", providing a clear and concise description of the error for debugging and logging purposes. - -**Note**: This method assumes that the `exception_node` attribute is properly initialized and contains a valid node object with accessible data. If the `exception_node` is not set or its data is not retrievable, this could lead to unexpected behavior or errors. - -**Output Example**: A possible return value of the __str__ method could be: -``` -ExecutionError: File not found -``` -This output indicates that the error message stored in the `exception_node` is "File not found". -*** -## ClassDef TraceMissingInputsError -**TraceMissingInputsError**: The TraceMissingInputsError class represents an exception that is raised when not all nodes used in the operator function are specified as inputs of the returned node. - -**Attributes**: -- message: A string representing the error message. - -**Code Description**: -The TraceMissingInputsError class is a subclass of the built-in Exception class. It is used to handle the case where not all nodes used in the operator function are specified as inputs of the returned node. - -The class has an `__init__` method that takes a `message` parameter and initializes the `message` attribute with the provided message. It also calls the `__init__` method of the parent Exception class with the message. - -The class also overrides the `__str__` method to return the error message when the exception is converted to a string. - -This exception is raised in the `forward` method of the `FunModule` class in the `opto.trace.bundle` module. The `forward` method is responsible for executing the operator function and handling any exceptions that occur during execution. If the `catch_execution_error` flag is set to `True`, the exception is caught and stored in the `outputs` variable. Otherwise, the exception is raised and propagated. - -**Note**: -- This exception is raised when not all nodes used in the operator function are specified as inputs of the returned node. -- The error message can be accessed through the `message` attribute of the exception object. - -**Output Example**: -``` -TraceMissingInputsError: Not all nodes used in the operator are specified as inputs of the returned node. Missing ['node_x'] -``` -### FunctionDef __init__(self, message) -**__init__**: The function of __init__ is to initialize an instance of the TraceMissingInputsError class with a specific error message. - -**parameters**: The parameters of this Function. -· message: A string that contains the error message to be associated with the TraceMissingInputsError instance. - -**Code Description**: The __init__ method is a constructor for the TraceMissingInputsError class. It takes a single parameter, `message`, which is a string representing the error message. Inside the method, the `message` parameter is assigned to the instance variable `self.message`. The constructor then calls the `__init__` method of its superclass using `super().__init__(self.message)`, passing the error message to the base class's constructor. This ensures that the error message is properly initialized and can be accessed through the standard exception handling mechanisms. - -**Note**: -- Ensure that the `message` parameter is a string to avoid type errors. -- This method is essential for setting up the error message that will be displayed when the TraceMissingInputsError is raised. -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to return the error message associated with the TraceMissingInputsError instance. - -**parameters**: The parameters of this Function. -· None: This method does not take any parameters. - -**Code Description**: The __str__ method in the TraceMissingInputsError class is designed to provide a human-readable representation of the error. When this method is called, it returns the value of the `message` attribute of the instance. This attribute typically contains a descriptive error message that explains the nature of the TraceMissingInputsError. The method ensures that when the error is printed or converted to a string, the message is displayed, making it easier for developers to understand the issue. - -**Note**: -- This method overrides the default __str__ method provided by Python's base Exception class. -- Ensure that the `message` attribute is properly set when initializing the TraceMissingInputsError instance to provide meaningful error information. - -**Output Example**: -If the `message` attribute of the TraceMissingInputsError instance is set to "Input data is missing", calling the __str__ method will return: -``` -"Input data is missing" -``` -*** diff --git a/generated_docs/opto/trace/modules.md b/generated_docs/opto/trace/modules.md deleted file mode 100644 index 6180a1b9..00000000 --- a/generated_docs/opto/trace/modules.md +++ /dev/null @@ -1,304 +0,0 @@ -## ClassDef NodeContainer -**NodeContainer**: The function of NodeContainer is to serve as an identifier for a container of nodes. - -**attributes**: The attributes of this Class. -· No specific attributes are defined within the provided code snippet. - -**Code Description**: The NodeContainer class is designed to act as a marker or identifier for objects that are containers of nodes. This class itself does not contain any specific attributes or methods, but it is used as a base class or type identifier in various parts of the project. - -In the project, NodeContainer is utilized in several contexts: - -1. **apply_op function in broadcast.py**: - - The apply_op function performs broadcasting operations on containers of nodes. It checks if the output is an instance of NodeContainer and recursively applies the operation to each attribute of the NodeContainer instance. This indicates that NodeContainer is used to group nodes together, allowing operations to be applied uniformly across all contained nodes. - -2. **to_data function in bundle.py**: - - The to_data function extracts data from nodes or containers of nodes. When the input object is an instance of NodeContainer, the function recursively extracts data from each attribute of the NodeContainer. This shows that NodeContainer is used to encapsulate nodes, enabling data extraction from complex structures. - -3. **ParameterContainer class in modules.py**: - - ParameterContainer inherits from NodeContainer and represents a container of parameter nodes. It includes methods to retrieve a flattened list of parameters and a dictionary of all parameters in the model. This inheritance indicates that ParameterContainer leverages the NodeContainer's role as a node container to manage parameter nodes specifically. - -4. **SubContainer and Container classes in test_apply_op.py**: - - Both SubContainer and Container classes inherit from NodeContainer. These classes initialize with various node attributes, demonstrating how NodeContainer can be extended to create more complex containers of nodes for testing purposes. - -**Note**: Points to note about the use of the code -- NodeContainer itself does not define any attributes or methods; it serves as a base class or type identifier. -- When extending NodeContainer, ensure that the derived classes properly encapsulate nodes to leverage the functionality provided by functions like apply_op and to_data. -- NodeContainer is integral to the project's handling of node containers, enabling consistent operations and data extraction across different types of node groupings. -## FunctionDef trainable_method(method) -**trainable_method**: The function of trainable_method is to determine if a given method is callable and has an attribute named "parameter". - -**parameters**: The parameters of this Function. -· method: The method to be checked for callability and the presence of the "parameter" attribute. - -**Code Description**: The trainable_method function is designed to check two specific conditions for a given method: -1. It verifies if the method is callable using the callable() function. -2. It checks if the method has an attribute named "parameter" using the hasattr() function. - -If both conditions are met, the function returns True; otherwise, it returns False. This function is particularly useful in scenarios where methods need to be filtered based on their trainability, which is indicated by the presence of the "parameter" attribute. - -In the context of its usage within the ParameterContainer class's parameters_dict method, trainable_method plays a crucial role. The parameters_dict method constructs a dictionary of all parameters in the model, including both trainable and non-trainable parameters. It iterates over the attributes of the ParameterContainer instance and uses trainable_method to identify methods that are both callable and have a "parameter" attribute. These methods are then included in the resulting dictionary with their "parameter" attribute values. - -**Note**: -- Ensure that the methods being checked are intended to have a "parameter" attribute if they are to be considered trainable. -- This function does not check the type or validity of the "parameter" attribute, only its presence. - -**Output Example**: -For a method that is callable and has a "parameter" attribute, trainable_method would return: -``` -True -``` -For a method that is either not callable or lacks a "parameter" attribute, trainable_method would return: -``` -False -``` -## ClassDef ParameterContainer -**ParameterContainer**: The function of ParameterContainer is to serve as a container for parameter nodes. - -**attributes**: -- No specific attributes are defined within the provided code snippet. - -**Code Description**: -The ParameterContainer class is a subclass of NodeContainer and represents a container of parameter nodes. It provides methods to retrieve a flattened list of parameters and a dictionary of all parameters in the model. - -The `parameters` method returns a flattened list of all the parameters in the model's `parameters_dict`. It iterates over the items in the `parameters_dict` and checks if each value is an instance of `ParameterNode` or `ParameterContainer`. If it is a `ParameterNode`, it appends it to the `parameters` list. If it is a `ParameterContainer`, it recursively calls the `parameters` method on the container and extends the `parameters` list with the result. If the value is neither a `ParameterNode` nor a `ParameterContainer`, it raises a `ValueError`. - -The `parameters_dict` method returns a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. It uses the `inspect.getmembers` function to get all the attributes of the `self` object. It then iterates over these attributes and checks if each attribute is a `functools.partial` object or a method attribute. If it is a `functools.partial` object, it retrieves the method from the `func` attribute and checks if it is a trainable method using the `trainable_method` function. If it is a trainable method, it adds the method's `parameter` attribute to the `parameters` dictionary with the attribute name as the key. If it is a method attribute, it checks if it is a trainable method using the `trainable_method` function and adds the method's `parameter` attribute to the `parameters` dictionary with the attribute name as the key. If the attribute is a `ParameterNode`, it adds it to the `parameters` dictionary with the attribute name as the key. If the attribute is a `ParameterContainer`, it adds it to the `parameters` dictionary with the attribute name as the key. Finally, it asserts that all the values in the `parameters` dictionary are instances of `ParameterNode` or `ParameterContainer`. - -The `parameters_dict` method is used to retrieve a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. It is called internally by the `parameters` method to retrieve the parameters dictionary. - -**Note**: -- The `ParameterContainer` class inherits from the `NodeContainer` class, which serves as an identifier for a container of nodes. -- The `ParameterContainer` class is designed to manage parameter nodes specifically, leveraging the functionality provided by the `NodeContainer` class. -- When using the `ParameterContainer` class, ensure that the derived classes properly encapsulate parameter nodes to ensure the correct functioning of the `parameters` and `parameters_dict` methods. - -**Output Example**: -```python -{ - 'param1': , - 'param2': , - 'container1': , - 'container2': -} -``` -### FunctionDef parameters(self) -**parameters**: The function of parameters is to return a flattened list of all the parameters in the model's parameters_dict, useful for optimization. - -**parameters**: The parameters of this function. -· self: The instance of the ParameterContainer class. - -**Code Description**: The parameters function is designed to collect and return a flattened list of all parameters contained within a model's parameters_dict. This is particularly useful for optimization tasks where a single list of parameters is required. - -1. The function initializes an empty list named parameters. -2. It then iterates over each key-value pair in the dictionary returned by the parameters_dict method of the ParameterContainer instance. -3. For each key-value pair: - - If the value is an instance of ParameterNode, it appends the value to the parameters list. - - If the value is an instance of ParameterContainer, it extends the parameters list with the result of calling the parameters method on that value. - - If the value is neither a ParameterNode nor a ParameterContainer, it raises a ValueError indicating that the model contains an unknown parameter type. -4. Finally, the function returns the populated parameters list. - -This method ensures that all parameters, whether they are directly part of the ParameterContainer or nested within other ParameterContainers, are included in a single, flattened list. - -**Note**: -- The function relies on the parameters_dict method to retrieve the dictionary of parameters. -- It assumes that all values in the parameters_dict are either instances of ParameterNode or ParameterContainer. Any other type will result in a ValueError. -- This function is essential for optimization processes that require a single list of all model parameters. - -**Output Example**: -A possible return value of the parameters function could be: -[ - , - , - ... -] -*** -### FunctionDef parameters_dict(self) -**parameters_dict**: The function of parameters_dict is to return a dictionary of all the parameters in the model, including both trainable and non-trainable parameters. - -**parameters**: -- self: The instance of the ParameterContainer class. - -**Code Description**: The parameters_dict method constructs a dictionary of all parameters in the model, including both trainable and non-trainable parameters. It iterates over the attributes of the ParameterContainer instance and checks each attribute using the trainable_method function. If the attribute is a class method and is trainable, it adds the method's "parameter" attribute to the dictionary. If the attribute is a method and is trainable, it adds the method's "parameter" attribute to the dictionary. If the attribute is a ParameterNode, it adds the ParameterNode object to the dictionary. If the attribute is a ParameterContainer, it adds the ParameterContainer object to the dictionary. - -The method then asserts that all values in the dictionary are either instances of ParameterNode or ParameterContainer. - -Finally, the method returns the constructed dictionary, which includes both trainable and non-trainable parameters. - -**Note**: -- The trainable_method function is used to determine if a given method is callable and has an attribute named "parameter". -- The method does not check the type or validity of the "parameter" attribute, only its presence. - -**Output Example**: -{ - 'param1': , - 'param2': , - ... -} -*** -## FunctionDef model(cls) -**model**: The function of model is to wrap a class with a decorator to help collect parameters for the optimizer. This decorated class cannot be pickled. - -**parameters**: The parameters of this Function. -· cls: The class to be wrapped by the decorator. - -**Code Description**: The `model` function is a decorator designed to wrap a given class, enhancing it to collect parameters for an optimizer. When a class is decorated with `model`, it is wrapped inside a new class called `ModelWrapper`, which inherits from both `Module` and the original class (`cls`). This allows the optimizer to access and manage the parameters of the class more effectively. However, it is important to note that classes decorated with `model` cannot be pickled, which may affect serialization and deserialization processes. - -The function is utilized in the project to facilitate the optimization process by ensuring that the parameters of the decorated class are properly managed. Although the specific usage within the project is not detailed in the provided documents, it is clear that the `model` function plays a crucial role in parameter management for optimization tasks. - -**Note**: -- Classes decorated with `model` cannot be pickled. -- Ensure that the class to be wrapped is compatible with the `Module` class. - -**Output Example**: -When a class `MyClass` is decorated with `model`, the resulting class `ModelWrapper` will inherit from both `Module` and `MyClass`, allowing the optimizer to collect and manage its parameters. The decorated class will look like this: - -```python -@model -class MyClass: - # class definition -``` - -This will result in a new class `ModelWrapper` that combines the functionalities of `Module` and `MyClass`. -### ClassDef ModelWrapper -**ModelWrapper**: The function of ModelWrapper is to serve as a specialized module that inherits functionalities from both the `Module` class and another class specified by `cls`. - -**attributes**: The attributes of this Class. -- No specific attributes are defined within the provided code snippet. - -**Code Description**: The `ModelWrapper` class is designed to extend the capabilities of the `Module` class by also inheriting from another class specified by `cls`. This dual inheritance allows `ModelWrapper` to combine the functionalities of both parent classes, making it a versatile component in the project. - -The `Module` class, from which `ModelWrapper` inherits, serves as a container for parameter nodes and provides essential methods such as `forward`, `__call__`, `save`, `load`, and `_set`. These methods facilitate the forward pass of the model, allow the module to be called as a function, and enable saving and loading of model parameters. - -By inheriting from `Module`, `ModelWrapper` gains access to these methods and functionalities. Additionally, the inheritance from `cls` allows `ModelWrapper` to incorporate any additional methods and attributes defined in `cls`, thereby enhancing its capabilities. - -**Note**: -- The `ModelWrapper` class does not define any new attributes or methods within the provided code snippet. It relies on the inherited functionalities from `Module` and `cls`. -- The `forward` method from the `Module` class must be implemented by any derived class to define the forward pass of the model. -- The `save` and `load` methods from the `Module` class can be used to save and load the parameters of the model to/from a file. -- The `_set` method from the `Module` class is a helper method used by the `load` method to set the parameters of the model. - -In summary, `ModelWrapper` is a flexible and extendable class that combines the functionalities of the `Module` class and another specified class, making it a powerful tool for managing model parameters and performing forward passes in a neural network or similar computational model. -*** -## ClassDef Module -**Module**: Module - -**attributes**: -- No specific attributes are defined within the provided code snippet. - -**Code Description**: -The `Module` class is a subclass of `ParameterContainer` and serves as a container for parameter nodes. It provides a `forward` method that needs to be implemented by derived classes. The `forward` method is responsible for performing the forward pass of the model. - -The `forward` method raises a `NotImplementedError` as it is meant to be overridden by derived classes. This method takes in `*args` and `**kwargs` as input parameters and should return the output of the forward pass. - -The `__call__` method is a convenience method that allows the `Module` object to be called as a function. It simply calls the `forward` method with the provided arguments and returns the result. - -The `save` method is used to save the parameters of the model to a file. It takes a `file_name` parameter as input and creates the necessary directory structure if it doesn't already exist. It then serializes the model's parameters using the `pickle` module and saves them to the specified file. - -The `load` method is used to load the parameters of the model from a file. It takes a `file_name` parameter as input and deserializes the parameters using the `pickle` module. The loaded parameters are then set as the new parameters of the model using the `_set` method. - -The `_set` method is a helper method used by the `load` method to set the parameters of the model from a dictionary. It takes a `new_parameters` parameter, which can be either a `ParameterContainer` or a parameter dictionary. It asserts that the `new_parameters` is of the correct type and then updates the model's parameters accordingly. - -**Note**: -- The `Module` class inherits from the `ParameterContainer` class, which serves as a container for parameter nodes. -- The `forward` method needs to be implemented by derived classes to define the forward pass of the model. -- The `save` and `load` methods can be used to save and load the parameters of the model to/from a file. -- The `_set` method is a helper method used by the `load` method to set the parameters of the model. - -**Output Example**: -```python -model = Module() -model.save("model_params.pkl") -model.load("model_params.pkl") -model.forward(input_data) -``` -### FunctionDef forward(self) -**forward**: The function of forward is to serve as an abstract method that must be implemented by subclasses of the Module class. - -**parameters**: The parameters of this Function. -· args: Variable length argument list. -· kwargs: Arbitrary keyword arguments. - -**Code Description**: The forward function is defined as a method within a class, and it is designed to be overridden by subclasses. The method takes any number of positional and keyword arguments, denoted by *args and **kwargs, respectively. However, in its current form, it raises a NotImplementedError, indicating that it is an abstract method. This means that any subclass inheriting from this class must provide its own implementation of the forward method. - -The forward method is called by the __call__ method of the same class. When an instance of the class is called like a function, the __call__ method is invoked, which in turn calls the forward method with the provided arguments. This design pattern is common in frameworks that require a standard interface for processing inputs, such as neural network layers in deep learning libraries. - -**Note**: -- The forward method must be implemented in any subclass; otherwise, calling an instance of the subclass will result in a NotImplementedError. -- Ensure that the implementation of the forward method in subclasses correctly handles the expected input arguments and performs the desired operations. -*** -### FunctionDef __call__(self) -**__call__**: The function of __call__ is to invoke the forward method of the Module class with the provided arguments. - -**parameters**: The parameters of this Function. -· args: Variable length argument list. -· kwargs: Arbitrary keyword arguments. - -**Code Description**: The __call__ method is designed to make instances of the Module class callable like a regular function. When an instance of the Module class is called, the __call__ method is triggered, which in turn calls the forward method with the same arguments. This design pattern is commonly used in frameworks that require a standard interface for processing inputs, such as neural network layers in deep learning libraries. - -The forward method, which must be implemented by any subclass of the Module class, is where the actual processing logic resides. The __call__ method acts as a wrapper that ensures the forward method is executed with the provided arguments. - -In the context of the project, the __call__ method is referenced by the __get__ method in the FunModule class located in opto\trace\bundle.py. The __get__ method uses functools.partial to bind the __call__ method to an instance of the Module class, effectively supporting instance methods. - -**Note**: -- The forward method must be implemented in any subclass of the Module class; otherwise, calling an instance of the subclass will result in a NotImplementedError. -- Ensure that the implementation of the forward method in subclasses correctly handles the expected input arguments and performs the desired operations. - -**Output Example**: The return value of the __call__ method depends on the implementation of the forward method in the subclass. For instance, if the forward method is implemented to perform a specific computation, the __call__ method will return the result of that computation. -*** -### FunctionDef save(self, file_name) -**save**: The function of save is to save the parameters of the model to a specified file. - -**parameters**: The parameters of this Function. -· file_name: The name of the file where the model parameters will be saved. - -**Code Description**: The save function is designed to persist the parameters of a model to a file. It first checks if the directory specified in the file_name exists. If the directory does not exist, it creates the directory using os.makedirs with the exist_ok=True flag to avoid raising an error if the directory already exists. The function then opens the specified file in binary write mode ("wb") and uses the pickle module to serialize and save the model's parameters. - -The parameters to be saved are obtained by calling the parameters_dict method on the instance (self). This method returns a dictionary containing all the parameters of the model, including both trainable and non-trainable parameters. The dictionary is then serialized and written to the file using pickle.dump. - -**Note**: -- Ensure that the file_name provided includes the correct path where the file should be saved. -- The directory will be created if it does not exist, so there is no need to manually create it beforehand. -- The parameters_dict method must be correctly implemented in the model to return all necessary parameters for saving. -- The file is opened in binary mode, so it will not be human-readable. Use pickle.load to deserialize the file when needed. -*** -### FunctionDef load(self, file_name) -**load**: The function of load is to load the parameters of the model from a file. - -**parameters**: The parameters of this function. -- file_name: The name of the file from which to load the model parameters. - -**Code Description**: The load function is responsible for loading the parameters of a model from a specified file. It takes a single parameter, file_name, which is the name of the file containing the model parameters. - -The function opens the specified file in binary read mode ("rb") using a with statement to ensure the file is properly closed after reading. It then uses the pickle.load function to deserialize the contents of the file into a Python object, which is stored in the variable loaded_data. - -After successfully loading the data, the function calls the _set method on the current instance (self) with loaded_data as the argument. The _set method is responsible for setting the parameters of the model using the loaded data. It ensures that the new parameters are valid and consistent with the existing parameters of the model by performing various checks and updates. - -**Note**: -- The file specified by file_name must exist and be accessible for reading. -- The contents of the file must be a valid serialized representation of the model parameters. -- The _set method is used to update the model's parameters with the loaded data, ensuring consistency and validity. -- Proper error handling should be implemented to handle cases where the file cannot be read or the contents are not as expected. -*** -### FunctionDef _set(self, new_parameters) -**_set**: The function of _set is to set the parameters of the model from a dictionary. - -**parameters**: -- self: The instance of the Module class. -- new_parameters: A ParameterContainer or a parameter dictionary containing the new parameters. - -**Code Description**: The _set function is responsible for setting the parameters of the model from a dictionary. It takes in the self parameter, which represents the instance of the Module class, and the new_parameters parameter, which can be either a ParameterContainer or a parameter dictionary. - -The function first asserts that the new_parameters parameter is an instance of either a dictionary or a ParameterContainer. If it is a ParameterContainer, it retrieves the parameters dictionary using the parameters_dict method. Otherwise, it assumes that new_parameters is already a dictionary. - -Next, it retrieves the current parameters dictionary using the parameters_dict method of the self object. - -The function then asserts that all the keys in the new_parameters_dict are present in the parameters_dict. This ensures that all the model parameters are included in the new parameters dictionary. - -After that, the function iterates over the items in the new_parameters_dict. For each key-value pair, it checks if the key exists in the parameters_dict. If it does, it asserts that the value is an instance of either a ParameterNode or a ParameterContainer. If it is a ParameterNode, it calls the _set method of the corresponding parameter in the parameters_dict, passing the value as the argument. This allows the parameter to update its value. If the key does not exist in the parameters_dict, it asserts that the key is not present in the __dict__ attribute of the self object. If this assertion passes, it sets the attribute of the self object with the key as the attribute name and the value as the attribute value. - -**Note**: -- The _set function is typically used to update the parameters of a model with new values. It ensures that the new parameters are valid and consistent with the existing parameters of the model. -- The function assumes that the model's parameters are stored in the parameters_dict, which is a dictionary of ParameterNodes or ParameterContainers. -- It is important to ensure that the new_parameters dictionary contains all the necessary parameters and that their values are of the correct type. -- The function relies on the _set method of ParameterNode to update the value of a parameter. -- The function uses the setattr function to dynamically set attributes on the self object. -*** diff --git a/generated_docs/opto/trace/nodes.md b/generated_docs/opto/trace/nodes.md deleted file mode 100644 index 2106c63d..00000000 --- a/generated_docs/opto/trace/nodes.md +++ /dev/null @@ -1,2213 +0,0 @@ -## FunctionDef node(message, name, trainable, constraint) -**node**: The function of node is to create a Node object from a message. If the message is already a Node, it will be returned as is. This function is provided for the convenience of the user and should be used instead of directly invoking the Node class. - -**parameters**: -- message: The message to create the Node from. -- name: (optional) The name of the Node. -- trainable: (optional) A boolean indicating whether the Node is trainable or not. Default is False. -- constraint: (optional) A constraint on the Node. - -**Code Description**: The node function is a versatile function that allows users to create Node objects from messages. It takes in a message and optional parameters such as name, trainable, and constraint. - -The function first checks if the trainable parameter is True. If it is, it checks if the message is already a Node. If it is, it extracts the underlying data and updates the name if a new name is provided. It then creates a ParameterNode object with the extracted data, name, trainable set to True, and the provided constraint. If the message is not already a Node, it creates a new ParameterNode object with the message as the data, the provided name, trainable set to True, and the provided constraint. - -If the trainable parameter is False, the function checks if the message is already a Node. If it is, it checks if a name is provided. If a name is provided, it issues a warning that the name is ignored because the message is already a Node. It then returns the message as is. If the message is not already a Node, it creates a new Node object with the message as the data, the provided name, and the provided constraint. - -**Note**: -- The node function is a convenient way to create Node objects from messages. -- The trainable parameter determines whether the created Node is trainable or not. -- The constraint parameter allows users to specify a constraint on the created Node. - -**Output Example**: A possible return value of the node function could be a ParameterNode object with the extracted data, name, trainable set to True, and the provided constraint. -## ClassDef Graph -**Graph**: The function of Graph is to serve as a registry of all the nodes, forming a Directed Acyclic Graph (DAG). - -**attributes**: The attributes of this Class. -· TRACE: A class-level attribute that determines whether the graph is traced when creating MessageNode. It is set to True by default. -· _nodes: An instance-level attribute, which is a defaultdict of lists, used as a lookup table to find nodes by name. - -**Code Description**: The Graph class is designed to manage and organize nodes in a Directed Acyclic Graph (DAG). It provides methods to register nodes, clear the graph, retrieve nodes by name, and identify root nodes. - -- The `__init__` method initializes the Graph object, setting up the `_nodes` attribute as a defaultdict of lists to store nodes by their names. - -- The `clear` method removes all nodes from the graph by deleting each node and reinitializing the `_nodes` attribute. - -- The `register` method adds a node to the graph. It ensures the node is an instance of the Node class and that its name follows the expected format (containing a colon). The method also handles name scoping and assigns a unique name to the node based on its position in the list. - -- The `get` method retrieves a node by its name, which includes an identifier. It splits the name to find the correct node in the `_nodes` dictionary. - -- The `roots` property returns a list of all root nodes in the graph. A root node is identified by its `is_root` attribute. - -- The `__str__` method provides a string representation of the `_nodes` attribute, useful for debugging and logging. - -- The `__len__` method returns the total number of nodes in the graph by summing the lengths of all lists in the `_nodes` dictionary. - -**Note**: -- The `TRACE` attribute controls whether the graph is traced during the creation of MessageNode instances. -- The `register` method assumes that elements in `_nodes` are never removed, which is important for maintaining the integrity of node names. - -**Output Example**: -- When calling the `__str__` method, the output might look like: - ``` - defaultdict(, {'node1': [], 'node2': []}) - ``` -- When calling the `__len__` method, the output might be: - ``` - 5 - ``` -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the Graph class by setting up an internal data structure to store nodes. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The __init__ function initializes an instance of the Graph class. It sets up an internal attribute `_nodes`, which is a defaultdict with lists as the default factory. This defaultdict will be used as a lookup table to store and retrieve nodes by their names. Each key in the `_nodes` dictionary represents a node name, and the corresponding value is a list of nodes associated with that name. This structure allows for efficient organization and retrieval of nodes within the graph. - -**Note**: -- The defaultdict from the collections module is used to automatically handle missing keys by initializing them with an empty list. -- This function does not require any arguments and is called automatically when a new instance of the Graph class is created. -*** -### FunctionDef clear(self) -**clear**: The function of clear is to remove all nodes from the graph and reset the internal node storage. - -**parameters**: The parameters of this Function. -· This function does not take any parameters other than the implicit self parameter. - -**Code Description**: The clear function is designed to empty the graph of all its nodes. It iterates over the current nodes stored in the _nodes attribute, which is a dictionary, and deletes each node. After all nodes have been deleted, it reinitializes the _nodes attribute to an empty defaultdict of lists. This ensures that the graph is completely cleared and ready to be repopulated with new nodes if necessary. - -The function is called in unit tests located in tests\unit_tests\test_backward.py and tests\unit_tests\test_optimizer.py. These tests likely use the clear function to reset the state of the graph between test cases, ensuring that each test runs with a clean slate and is not affected by the state left by previous tests. - -**Note**: -- This function should be used with caution as it irreversibly deletes all nodes in the graph. -- After calling clear, any references to the previously stored nodes will become invalid. -- Ensure that any necessary data is saved or processed before calling this function, as it will reset the graph's state completely. -*** -### FunctionDef register(self, node) -**register**: The function of register is to add a node to the graph. - -**parameters**: -- self: The instance of the class. -- node: The node object to be registered in the graph. - -**Code Description**: -The `register` function is a method of the `Graph` class in the `nodes.py` file of the `trace` module. It is used to add a node to the graph. The function takes in the `self` parameter, which represents the instance of the class, and the `node` parameter, which is the node object to be registered. - -The function first checks if the `node` parameter is an instance of the `Node` class using the `isinstance` function. If it is not, an `AssertionError` is raised. - -Next, the function checks if the name of the node contains exactly one ":" character by splitting the name using the ":" delimiter and checking the length of the resulting list. If the length is not equal to 2, an `AssertionError` is raised. This check ensures that the name of the node follows the required format. - -After that, the function splits the name of the node using the ":" delimiter and assigns the first part of the split to the `name` variable. This is done to separate the name from the version number. - -The function then checks if there are any name scopes defined in the `NAME_SCOPES` list. If the length of the list is greater than 0, the name is prefixed with the last scope in the list followed by a "/". This allows for scoping of node names. - -Finally, the function adds the node to the `_nodes` dictionary using the modified name as the key. The `_name` attribute of the node is set to the modified name followed by the index of the node in the list of nodes with the same name. This index is obtained by subtracting 1 from the length of the list of nodes with the same name. - -**Note**: -- The `register` function should only be called after the node has been properly initialized and its name has been set. -- The function assumes that elements in the `_nodes` dictionary never get removed. - -**Output Example**: -If the name of the node is "node:0", the `register` function will add the node to the `_nodes` dictionary with the key "node" and set the `_name` attribute of the node to "node:0". -*** -### FunctionDef get(self, name) -**get**: The function of get is to retrieve a specific node from the graph based on a given name and identifier. - -**parameters**: The parameters of this Function. -· name: A string in the format "name:id", where "name" is the name of the node and "id" is the identifier of the node. - -**Code Description**: The get function is designed to extract and return a specific node from a graph structure. The input parameter 'name' is expected to be a string formatted as "name:id". The function first splits this string into two parts: 'name' and 'id', using the colon (":") as the delimiter. The 'name' part represents the name of the node, and the 'id' part represents the identifier of the node, which is then converted to an integer. The function then accesses the '_nodes' dictionary attribute of the graph object, using the 'name' as the key to retrieve the list of nodes associated with that name. Finally, it returns the node at the position specified by the integer 'id' within that list. - -**Note**: -- Ensure that the 'name' parameter is correctly formatted as "name:id" before calling this function. -- The function assumes that the '_nodes' attribute is a dictionary where each key is a node name and the corresponding value is a list of nodes. -- The 'id' should be a valid index within the list of nodes for the given 'name'. - -**Output Example**: -If the '_nodes' dictionary is structured as follows: -```python -_nodes = { - "nodeA": ["nodeA_0", "nodeA_1"], - "nodeB": ["nodeB_0"] -} -``` -Calling `get("nodeA:1")` would return `"nodeA_1"`. -*** -### FunctionDef roots(self) -**roots**: The function of roots is to return a list of root nodes from the graph. - -**parameters**: This function does not take any parameters. - -**Code Description**: The `roots` function iterates over the values in the `_nodes` dictionary of the `Graph` object. The `_nodes` dictionary contains lists of nodes. For each node in these lists, the function checks if the node is a root node by evaluating the `is_root` attribute of the node. If the `is_root` attribute is `True`, the node is included in the resulting list. The function ultimately returns a list of all nodes that are identified as root nodes. - -**Note**: -- Ensure that the nodes in the `_nodes` dictionary have the `is_root` attribute properly set to `True` for root nodes and `False` for non-root nodes. -- The function assumes that `_nodes` is a dictionary where the values are lists of node objects. - -**Output Example**: -If the `_nodes` dictionary contains the following structure: -```python -_nodes = { - 'group1': [Node1, Node2], - 'group2': [Node3, Node4] -} -``` -and `Node1` and `Node3` have their `is_root` attribute set to `True`, while `Node2` and `Node4` have it set to `False`, the `roots` function will return: -```python -[Node1, Node3] -``` -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to return a string representation of the Graph object. - -**parameters**: The parameters of this Function. -· None: This method does not take any parameters. - -**Code Description**: The __str__ method is a special method in Python that is used to define the string representation of an object. In this implementation, the __str__ method returns the string representation of the `_nodes` attribute of the Graph object. The `_nodes` attribute is expected to be a collection (such as a list or dictionary) that holds the nodes of the graph. By converting `_nodes` to a string, the method provides a human-readable format of the graph's nodes, which can be useful for debugging and logging purposes. - -**Note**: -- Ensure that the `_nodes` attribute is properly initialized and contains the nodes of the graph before calling the __str__ method. -- The readability and usefulness of the output depend on the structure and content of the `_nodes` attribute. - -**Output Example**: -If the `_nodes` attribute is a list of node names, such as `['A', 'B', 'C']`, the __str__ method will return the string "['A', 'B', 'C']". If `_nodes` is a dictionary representing nodes and their connections, such as `{'A': ['B', 'C'], 'B': ['A'], 'C': ['A']}`, the method will return the string "{'A': ['B', 'C'], 'B': ['A'], 'C': ['A']}". -*** -### FunctionDef __len__(self) -**__len__**: The function of __len__ is to return the number of nodes in the graph. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Graph class. - -**Code Description**: The __len__ method calculates the total number of nodes in the graph. It does this by iterating over the values in the self._nodes dictionary, where each value is a list representing the connections or edges of a particular node. The method uses a list comprehension to get the length of each list (i.e., the number of connections for each node) and then sums these lengths to get the total number of nodes. Finally, it returns this sum as the result. - -**Note**: This method assumes that the self._nodes attribute is a dictionary where each key is a node and each value is a list of connections for that node. The method will not work correctly if self._nodes is not structured in this way. - -**Output Example**: If the graph has 3 nodes with the following connections: -- Node A connected to Node B and Node C -- Node B connected to Node A -- Node C connected to Node A - -The return value of __len__ would be 3. -*** -## ClassDef AbstractNode -**AbstractNode**: The function of AbstractNode is to represent an abstract data node in a directed graph. - -**attributes**: -- `data`: The data stored in the node. -- `parents`: The list of parent nodes. -- `children`: The list of child nodes. -- `name`: The name of the node. -- `py_name`: The name of the node without the ":" character. -- `id`: The ID of the node. -- `level`: The level of the node in the graph. -- `is_root`: A boolean indicating whether the node is a root node. -- `is_leaf`: A boolean indicating whether the node is a leaf node. - -**Code Description**: The `AbstractNode` class represents an abstract data node in a directed graph. It is a generic class that can store any type of data. The node can have multiple parents and children, forming a directed graph structure. The node has a name, which is used to identify it within the graph. The `py_name` attribute is the same as the name attribute, but with the ":" character removed. The `id` attribute is extracted from the name and represents a version number. - -The node can be initialized with a value, an optional name, and an optional trainable flag. If the value is an instance of the `Node` class, the node will be initialized as a reference to that node, otherwise, the value will be stored directly in the node. The default name is generated based on the type of the value and a version number. - -The `AbstractNode` class provides several properties to access its attributes. The `data` property allows access to the stored data. If the node is being traced within a context, the `data` property adds the node to the list of used nodes. The `parents` property returns a list of parent nodes, and the `children` property returns a list of child nodes. The `name` property returns the name of the node, and the `py_name` property returns the name without the ":" character. The `id` property returns the version number extracted from the name. The `level` property returns the level of the node in the graph. The `is_root` property returns True if the node has no parents, and the `is_leaf` property returns True if the node has no children. - -The `AbstractNode` class also provides internal methods to add parents and children to the node. The `_add_child` method adds a child node to the node's list of children. The `_add_parent` method adds a parent node to the node's list of parents and updates the level of the node based on the parent's level. - -The `AbstractNode` class overrides the `__str__` method to provide a string representation of the node. The representation includes the name, the type of the data, and the data itself. - -The `AbstractNode` class implements the `__deepcopy__` method to create a deep copy of the node. This allows the node to be detached from the original graph. - -The `AbstractNode` class provides comparison methods `lt` and `gt` to compare the levels of two nodes. - -**Note**: The `AbstractNode` class is meant to be subclassed and extended to create specific types of nodes. - -**Output Example**: -``` -Node: (node_name, dtype=, data=10) -``` -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize an instance of the AbstractNode class. - -**parameters**: -- self: The instance of the class. -- value: The value to be assigned to the node. -- name: The name of the node (optional). -- trainable: A boolean indicating whether the node is trainable or not (optional). - -**Code Description**: -The `__init__` function is the constructor of the AbstractNode class. It takes in the `self` parameter, which represents the instance of the class, and the `value`, `name`, and `trainable` parameters, which are used to initialize the attributes of the node. - -The function starts by initializing the `_parents`, `_children`, and `_level` attributes to empty lists and 0 respectively. These attributes are used to keep track of the parent and child nodes of the current node, as well as the level of the node in the graph. - -Next, the function generates a default name for the node based on the type of the `value` parameter. If the `name` parameter is provided, it is appended to the default name. The format of the name is "type:version", where the version is set to 0 if no name is provided. - -After that, the function checks if the `value` parameter is an instance of the Node class. If it is, the `_data` attribute of the current node is set to the `_data` attribute of the `value` parameter, and the `_name` attribute is set to the `_name` attribute of the `value` parameter if no name is provided. Otherwise, the `_data` attribute is set to the `value` parameter itself, and the `_name` attribute is set to the default name. - -Finally, the function calls the `register` function of the GRAPH object to register the current node in the graph. - -**Note**: -- The `__init__` function should be called to create a new instance of the AbstractNode class. -- The `value` parameter can be any type of value. -- The `name` parameter is optional and can be used to provide a custom name for the node. -- The `trainable` parameter is optional and can be used to indicate whether the node is trainable or not. -- The `register` function should only be called after the node has been properly initialized and its name has been set. -*** -### FunctionDef data(self) -**data**: The function of data is to retrieve the internal data of a node, potentially adding the node to a list of used nodes if certain conditions are met. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the class that contains this method. - -**Code Description**: The data function is designed to return the internal data of a node object. It first checks if there are any nodes in the USED_NODES list and if the GRAPH.TRACE flag is set to True. If both conditions are met, it adds the current node (self) to the USED_NODES list. This indicates that the node is being used within a tracing context. Finally, the function returns the value of the node's internal data by accessing the "_data" attribute. - -This function is utilized in various parts of the project to access the data stored within nodes. For instance: -- In the node_to_function_feedback function in opto\optimizers\function_optimizer.py, it retrieves node data to convert a TraceGraph to a FunctionFeedback. -- In the construct_update_dict method of the FunctionOptimizer class, it converts suggestions into the appropriate data types by accessing node data. -- In the __next__ method of the SeqIterable class in opto\trace\containers.py, it iterates over a wrapped list of nodes and accesses their data. -- In the ExecutionError class's __init__ and __str__ methods in opto\trace\errors.py, it retrieves the data of an exception node to initialize and represent the error. -- In the get_label method of the NodeVizStyleGuide class in opto\trace\nodes.py, it generates labels for nodes by accessing their data. -- In the _set method of the Node class in opto\trace\nodes.py, it sets the value of a node, unwrapping it if necessary. -- In the trace_fun method of the Foo class in tests\unit_tests\test_bundle.py, it prints the data of a node during a trace function. - -**Note**: This function assumes that the "_data" attribute exists within the node object. If this attribute is not present, an AttributeError will be raised. - -**Output Example**: A possible return value of the code could be any data type stored in the "_data" attribute of the node, such as an integer, string, list, or custom object. For example, if the "_data" attribute contains the integer 42, the function will return 42. -*** -### FunctionDef parents(self) -**parents**: The function of parents is to return the parents of the current node. -**parameters**: -- self: The current node object. -**Code Description**: -The `parents` function is a method of the `AbstractNode` class in the `nodes.py` module. It returns the parents of the current node. The parents are stored in the `_parents` attribute of the node object. - -The function takes only one parameter, `self`, which refers to the current node object. It is used to access the `_parents` attribute and return its value. - -The `_parents` attribute is a list that contains the parent nodes of the current node. These parent nodes are the nodes that have an edge pointing to the current node in the graph. - -The `parents` function is called by several objects in the project. For example, it is called by the `is_root` function in the `AbstractNode` class, which checks if the current node is a root node by checking if it has any parents. It is also called by the `backward` function in the `Node` class, which performs a backward pass in the graph by propagating feedback from the current node to its parents. - -**Note**: The `parents` function is a basic method that provides access to the parents of a node. It is an essential part of the graph structure and is used in various operations such as graph traversal and feedback propagation. - -**Output Example**: -If the current node has two parents, the `parents` function will return a list containing the two parent nodes. -*** -### FunctionDef children(self) -**children**: The function of children is to return the list of child nodes associated with the current node. - -**parameters**: This function does not take any parameters. - -**Code Description**: The `children` function is a method of the `AbstractNode` class. It returns the `_children` attribute of the instance, which is a list containing the child nodes of the current node. This method is essential for accessing the hierarchical structure of nodes, allowing traversal and manipulation of the node tree. - -The `children` method is called by the `is_leaf` method within the same `AbstractNode` class. The `is_leaf` method uses `children` to determine if the current node is a leaf node (i.e., it has no children). Specifically, `is_leaf` checks if the length of the list returned by `children` is zero, indicating that the node has no children and is therefore a leaf. - -Additionally, the `children` method is referenced in the `opto\trace\bundle.py` file and the `tests\unit_tests\test_nodes.py` file, although specific details of its usage in these files are not provided. - -**Note**: Ensure that the `_children` attribute is properly initialized and maintained within the `AbstractNode` class to avoid unexpected behavior when calling the `children` method. - -**Output Example**: A possible return value of the `children` method could be: -```python -[, ] -``` -This indicates that the current node has two child nodes, each represented by an instance of `AbstractNode`. -*** -### FunctionDef name(self) -**name**: The function of name is name. -**parameters**: -- self: The instance of the class. -**Code Description**: -The `name` function is a method of the `AbstractNode` class. It returns the value of the private attribute `_name`. This function is used to retrieve the name of the node. - -The `_name` attribute is set when the node is registered in the graph. It is a combination of the node's name and its index in the list of nodes with the same name. The index is incremented each time a new node with the same name is registered. - -This function is called by various objects in the project. For example, it is called by the `get_fun_name` function in the `function_optimizer.py` file of the `optimizers` module. It is also called by the `register` function in the `nodes.py` file of the `trace` module. - -In the `get_fun_name` function, the `name` function is used to retrieve the name of a `MessageNode` object. If the `info` attribute of the node is a dictionary and it contains the key "fun_name", the value associated with that key is returned. Otherwise, the name of the node is split using the ":" delimiter, and the first part of the split is returned. - -In the `register` function, the `name` function is used to set the `_name` attribute of a node. The name is split using the ":" delimiter, and the first part of the split is assigned to the `name` variable. If there are any name scopes defined in the `NAME_SCOPES` list, the name is prefixed with the last scope in the list followed by a "/". The node is then added to the `_nodes` dictionary using the modified name as the key. The `_name` attribute of the node is set to the modified name followed by the index of the node in the list of nodes with the same name. - -**Note**: -- The `name` function should only be called after the node has been registered in the graph. -- The `name` function assumes that elements in the `_nodes` dictionary never get removed. - -**Output Example**: -If the `_name` attribute of a node is "node:0", the `name` function will return "node:0". -*** -### FunctionDef py_name(self) -**py_name**: The function of py_name is py_name. - -**parameters**: -- self: The instance of the class. - -**Code Description**: -The `py_name` function is a method of the current class. It returns the value of the `name` attribute after removing the ":" character. This function is used to modify the name attribute by replacing the ":" character with an empty string. - -This function is called by various objects in the project. For example, it is called by the `repr_function_call` function in the `function_optimizer.py` file of the `optimizers` module. It is also called by the `node_to_function_feedback` function in the same file. - -In the `repr_function_call` function, the `py_name` function is used to retrieve the name of a `MessageNode` object. The name is then used to construct a function call string. - -In the `node_to_function_feedback` function, the `py_name` function is used to retrieve the name of a node. The name is then used as a key in the `documentation` dictionary. - -In the `summarize` method of the `FunctionOptimizer` class, the `py_name` function is used to retrieve the name of a parameter node. The name is then used to classify the node into variables and others. - -In the `construct_update_dict` method of the `FunctionOptimizer` class, the `py_name` function is used to retrieve the name of a parameter node. The name is then used to construct an update dictionary. - -In the `fun` method of the `FunModule` class, the `py_name` function is used to retrieve the name of a parameter node. The name is then used to define a function. - -In the `get_label` method of the `NodeVizStyleGuide` class, the `py_name` function is used to retrieve the name of a node. The name is then used to construct a label for the node. - -In the `backward` method of the `Node` class, the `py_name` function is used to retrieve the name of a node. The name is then used for visualization purposes. - -**Note**: -- The `py_name` function should only be called after the name attribute has been set. -- The `py_name` function assumes that the name attribute does not contain any other special characters that need to be replaced. - -**Output Example**: -If the name attribute of a node is "node:0", the `py_name` function will return "node0". -*** -### FunctionDef id(self) -**id**: The function of id is to extract and return the identifier part of the node's name. - -**parameters**: The parameters of this Function. -- self: The instance of the class. - -**Code Description**: The `id` function is a method of the `AbstractNode` class. It operates on the `name` attribute of the instance, which is a string formatted as "name:identifier". The function splits this string using the colon (":") delimiter and returns the second part, which corresponds to the identifier. This identifier is typically a unique part of the node's name, distinguishing it from other nodes with the same base name. - -The `name` attribute is accessed through the `name` method of the `AbstractNode` class, which retrieves the value of the private attribute `_name`. The `id` function relies on the assumption that the `name` attribute follows the "name:identifier" format. - -**Note**: -- The `id` function should only be called after the node's `name` attribute has been properly set and follows the expected format. -- Ensure that the `name` attribute contains a colon (":") to avoid index errors during the split operation. - -**Output Example**: -If the `name` attribute of a node is "node:0", the `id` function will return "0". -*** -### FunctionDef level(self) -**level**: The function of level is to return the internal level attribute of the object. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The level function is a method that returns the value of the private attribute _level of the object. This method is used to access the internal state of the object, specifically the _level attribute, which is presumably set elsewhere in the class. The function does not modify any state or take any arguments; it simply provides a way to retrieve the current value of _level. - -In the context of its usage within the project, the level function is called by the init_feedback method in the GraphPropagator class, located in the opto\trace\propagators\graph_propagator.py file. The init_feedback method uses the level function to obtain the level of a node and includes this information in the TraceGraph it constructs. This indicates that the level of a node is an important piece of information for initializing feedback in the graph propagation process. - -**Note**: This function is a simple accessor and does not perform any validation or modification of the _level attribute. It is important to ensure that the _level attribute is properly initialized before calling this function to avoid potential issues. - -**Output Example**: If the _level attribute of the object is set to 3, calling the level function will return 3. -*** -### FunctionDef is_root(self) -**is_root**: The function of is_root is to determine if the current node is a root node. - -**parameters**: The parameters of this function. -· self: The current node object. - -**Code Description**: The `is_root` function is a method of the `AbstractNode` class in the `nodes.py` module. It checks whether the current node is a root node by evaluating the length of its parents list. Specifically, it returns `True` if the length of the parents list is zero, indicating that the node has no parents and is therefore a root node. Conversely, it returns `False` if the node has one or more parents. - -The function relies on the `parents` method of the `AbstractNode` class to retrieve the list of parent nodes. The `parents` method accesses the `_parents` attribute of the node object, which is a list containing the parent nodes. By checking the length of this list, the `is_root` function determines the root status of the node. - -**Note**: This function is essential for identifying root nodes in a graph structure, which can be useful for various graph operations such as traversal, initialization, and feedback propagation. - -**Output Example**: -- If the current node has no parents, the `is_root` function will return `True`. -- If the current node has one or more parents, the `is_root` function will return `False`. -*** -### FunctionDef is_leaf(self) -**is_leaf**: The function of is_leaf is to determine if the current node is a leaf node, meaning it has no children. - -**parameters**: This function does not take any parameters. - -**Code Description**: The `is_leaf` method is a part of the `AbstractNode` class. It checks whether the current node has any child nodes by utilizing the `children` method of the same class. Specifically, it returns `True` if the length of the list returned by the `children` method is zero, indicating that the node has no children and is therefore a leaf node. Otherwise, it returns `False`. - -The `children` method, which is called within `is_leaf`, returns the `_children` attribute of the instance. This attribute is a list containing the child nodes of the current node. By checking the length of this list, `is_leaf` can accurately determine the leaf status of the node. - -**Note**: Ensure that the `_children` attribute is properly initialized and maintained within the `AbstractNode` class to avoid unexpected behavior when calling the `is_leaf` method. - -**Output Example**: A possible return value of the `is_leaf` method could be: -```python -True -``` -This indicates that the current node has no children and is therefore a leaf node. -*** -### FunctionDef _add_child(self, child) -**_add_child**: The function of _add_child is to add a child node to the current node. -**parameters**: -- child: The child node to be added. - -**Code Description**: -The `_add_child` function is used to add a child node to the current node. It performs the following steps: -1. It first checks if the child node is not the same as the current node itself. If it is, it raises an assertion error with the message "Cannot add self as a child." -2. It then checks if the child node is an instance of the `Node` class. If it is not, it raises an assertion error with a message indicating that the child is not a Node. -3. Finally, it calls the `_add_parent` function of the child node, passing the current node as the parent. - -**Note**: -- The `_add_child` function ensures that the child node is not the same as the current node and that it is an instance of the `Node` class before adding it as a child. -- This function assumes that the child node has an `_add_parent` function to add the current node as its parent. -*** -### FunctionDef _add_parent(self, parent) -**_add_parent**: The function of _add_parent is to add a parent node to the current node in the hierarchical structure of the graph. - -**parameters**: -- parent: The parent node to be added. - -**Code Description**: -The _add_parent function is a method designed to add a parent node to the current node in the hierarchical structure of the graph. It performs several checks and operations to ensure the validity of the parent node and the consistency of the graph structure. - -First, the function asserts that the parent node is not the same as the current node, as it is not allowed to add itself as a parent. This check prevents circular dependencies and ensures the integrity of the graph. - -Next, the function asserts that the parent node is an instance of the Node class. This check ensures that only valid nodes can be added as parents. - -If both checks pass, the function proceeds to add the current node as a child to the parent node by appending it to the parent's _children attribute. Similarly, it adds the parent node to the current node's _parents attribute. - -Finally, the function calls the _update_level method to update the level attribute of the current node. It passes the maximum value between the current node's _level attribute and the parent node's _level attribute plus one as the new level value. This ensures that the hierarchical structure of the nodes is maintained correctly, with child nodes always having a level greater than or equal to their parent nodes. - -It is worth noting that the _add_parent function assumes that the parent parameter is a valid instance of the Node class. If the parent parameter is not a Node instance, an assertion error will be raised. - -**Note**: -- The function does not return any value. -- The function assumes that the parent parameter is a valid instance of the Node class. -- The function raises an assertion error if the parent parameter is the same as the current node or if it is not an instance of the Node class. -*** -### FunctionDef _update_level(self, new_level) -**_update_level**: The function of _update_level is to update the level attribute of the current node to a new specified level. - -**parameters**: The parameters of this Function. -· new_level: The new level to which the node's level attribute should be updated. - -**Code Description**: The _update_level function is a method designed to update the internal _level attribute of an instance of the AbstractNode class. This method takes a single parameter, new_level, which represents the new level value that the node should be assigned. The function directly assigns this new value to the node's _level attribute. - -In the context of its usage within the project, the _update_level function is called by the _add_parent method of the AbstractNode class. When a new parent node is added to the current node, the _add_parent method ensures that the current node's level is updated appropriately. Specifically, it sets the current node's level to the maximum of its current level and the new parent's level plus one. This ensures that the hierarchical structure of the nodes is maintained correctly, with child nodes always having a level greater than or equal to their parent nodes. - -**Note**: -- The function assumes that the new_level parameter is a valid integer representing the level. -- The function does not perform any validation or checks on the new_level parameter; it directly assigns it to the _level attribute. -- The commented-out line in the function suggests that there was an intention to update a global or shared structure (GRAPH._levels) that tracks nodes by their levels, but this functionality is not implemented in the current version of the function. -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to provide a string representation of the AbstractNode object. - -**parameters**: The parameters of this function. -· self: The instance of the AbstractNode class. - -**Code Description**: The __str__ method in the AbstractNode class returns a string that represents the node in a human-readable format. This method is particularly useful for debugging and logging purposes, as it provides a quick way to inspect the node's key attributes. The string includes the node's name, the data type of the node's data, and the actual data stored in the node. - -The method constructs the string by accessing the `name` property of the node, which retrieves the node's name. It also accesses the `_data` attribute to include the data type and the data itself in the string. The `name` property is a method that returns the value of the private attribute `_name`, which is set when the node is registered in the graph. - -**Note**: -- The __str__ method should be used when a readable string representation of the node is needed, such as in logging or debugging scenarios. -- Ensure that the node has been properly initialized and registered before calling this method to avoid any unexpected behavior. - -**Output Example**: -If a node has the name "node:0", its data type is ``, and its data is `42`, the __str__ method will return: -``` -Node: (node:0, dtype=, data=42) -``` -*** -### FunctionDef __deepcopy__(self, memo) -**__deepcopy__**: The function of __deepcopy__ is to create a deep copy of the node, which is detached from the original graph. - -**parameters**: The parameters of this Function. -· memo: A dictionary used to keep track of objects that have already been copied to avoid infinite recursion during the deep copy process. - -**Code Description**: The __deepcopy__ function is designed to create a deep copy of an instance of the AbstractNode class. This means that the new instance will be a completely independent copy of the original, with no shared references to mutable objects. - -1. The function starts by obtaining the class of the current instance (`cls = self.__class__`). -2. It then creates a new, uninitialized instance of this class (`result = cls.__new__(cls)`). -3. The `memo` dictionary is updated to associate the original instance's ID with the new instance (`memo[id(self)] = result`). This helps in tracking already copied objects to prevent infinite loops. -4. The function iterates over all the attributes of the original instance (`for k, v in self.__dict__.items():`). -5. For attributes named `_parents` or `_children`, it sets these attributes in the new instance to empty lists (`setattr(result, k, [])`). This ensures that the new instance starts with no parent or child nodes. -6. For all other attributes, it performs a deep copy of the attribute's value and assigns it to the new instance (`setattr(result, k, copy.deepcopy(v, memo))`). -7. Finally, the new instance is returned (`return result`). - -**Note**: -- This function ensures that the new node is completely independent of the original node, with no shared references to mutable objects. -- Special handling is provided for `_parents` and `_children` attributes to ensure they are initialized as empty lists in the new instance. - -**Output Example**: -If the original node has attributes like `name`, `_parents`, and `_children`, the deep copy will result in a new node with the same `name` but with `_parents` and `_children` set to empty lists. For example: - -Original Node: -```python -original_node = AbstractNode() -original_node.name = "Node1" -original_node._parents = [parent_node] -original_node._children = [child_node] -``` - -Deep Copied Node: -```python -copied_node = copy.deepcopy(original_node) -print(copied_node.name) # Output: Node1 -print(copied_node._parents) # Output: [] -print(copied_node._children) # Output: [] -``` -*** -### FunctionDef lt(self, other) -**lt**: The function of lt is to compare the levels of two nodes and determine if the level of the current node is less than the level of another node. - -**parameters**: The parameters of this Function. -· self: The current instance of the node. -· other: Another instance of a node to compare with the current node. - -**Code Description**: The lt function is a method used to compare the levels of two nodes. It takes two parameters: `self`, which refers to the current node instance, and `other`, which refers to another node instance. The function compares the `_level` attribute of both nodes. Specifically, it checks if the negated level of the current node (`-self._level`) is less than the negated level of the other node (`-other._level`). This effectively means that the function is comparing the levels in reverse order, where a higher numerical level is considered "less than" a lower numerical level. - -**Note**: -- Ensure that both `self` and `other` have the `_level` attribute defined before using this function. -- This function is intended to be used where node levels are compared in a reversed manner. - -**Output Example**: -If `self._level` is 3 and `other._level` is 5, the function will return `True` because `-3` is less than `-5`. -*** -### FunctionDef gt(self, other) -**gt**: The function of gt is to compare the levels of two AbstractNode objects and determine if the level of the current object is greater than the level of another object. - -**parameters**: The parameters of this Function. -· self: The instance of the current AbstractNode object. -· other: Another instance of an AbstractNode object to compare against. - -**Code Description**: The gt function is a method used to compare the levels of two AbstractNode objects. It takes two parameters: `self` and `other`, which are both instances of AbstractNode. The function compares the `_level` attribute of the two objects. Specifically, it negates the `_level` attributes of both objects and then checks if the negated level of the current object (`self`) is greater than the negated level of the other object (`other`). This effectively determines if the level of the current object is greater than the level of the other object. - -**Note**: -- The `_level` attribute must be defined for both AbstractNode objects being compared. -- This function relies on the assumption that `_level` is a numeric value that can be meaningfully compared. - -**Output Example**: -If `self._level` is 3 and `other._level` is 2, the function will return `True` because -3 is greater than -2. -*** -## FunctionDef get_op_name(description) -**get_op_name**: The function of get_op_name is to extract the operator type from the given description. - -**Parameters**: -- description: A string representing the description from which the operator type needs to be extracted. - -**Code Description**: -The `get_op_name` function takes a description as input and uses regular expression to search for the operator type enclosed in square brackets at the beginning of the description. If a match is found, the operator type is extracted and returned. Otherwise, a `ValueError` is raised with a specific error message. - -This function is called by multiple objects in the project. In the `FunModule` class of the `bundle.py` file, the `get_op_name` function is used to generate the description for the function module. The extracted operator type is combined with the function name and docstring to create a meaningful description. The `name` method of the `FunModule` class also calls the `get_op_name` function to retrieve the operator type from the description. - -The `get_op_name` function is also used in the `backward` method of the `Node` class in the `nodes.py` file. This method performs a backward pass in a graph and propagates feedback from child nodes to parent nodes. The `get_op_name` function is used to extract the operator type from the description of each node. - -**Note**: -- The description parameter must contain the operator type enclosed in square brackets at the beginning. -- If the description does not contain the operator type, a `ValueError` will be raised. - -**Output Example**: -If the description is "[Add] Add two numbers", the function will return "Add". -## ClassDef NodeVizStyleGuide -**NodeVizStyleGuide**: The function of NodeVizStyleGuide is to provide a standardized way to visualize nodes in a graph, particularly for use with graph visualization tools like Graphviz. - -**attributes**: The attributes of this Class. -· style: A string that defines the style of the visualization. Default is 'default'. -· print_limit: An integer that sets the maximum number of characters to print for node descriptions and content. Default is 100. - -**Code Description**: The NodeVizStyleGuide class is designed to facilitate the visualization of nodes in a graph by providing a consistent style guide. It includes methods to generate attributes for nodes, such as labels, shapes, colors, and styles, which are essential for rendering nodes in a visually coherent manner. - -- The `__init__` method initializes the class with a specified style and a print limit for node descriptions and content. -- The `get_attrs` method returns a dictionary of attributes for a given node, including label, shape, fill color, and style. -- The `get_label` method constructs a label for a node by combining its name, description, and data. It truncates the description and data if they exceed the print limit. -- The `get_node_shape` method determines the shape of a node based on its type. For instance, ParameterNode types are represented as 'box', while other types are represented as 'ellipse'. -- The `get_color` method assigns a color to a node based on its type. ExceptionNode types are colored 'firebrick1', and ParameterNode types are colored 'lightgray'. -- The `get_style` method sets the style of a node to 'filled,solid' if the node is trainable; otherwise, it returns an empty string. - -In the context of its usage within the project, the NodeVizStyleGuide class is utilized in the `backward` method of the Node class. When the `visualize` parameter is set to True, an instance of NodeVizStyleGuide is created to generate the necessary attributes for each node in the graph. These attributes are then used to render the nodes and edges in the graph using Graphviz. The `get_attrs` method is called to obtain the visualization attributes for each node, ensuring that the graph is displayed with a consistent and informative style. - -**Note**: -- Ensure that the `print_limit` is set appropriately to avoid truncating important information in node descriptions and content. -- The class assumes the existence of specific node types like ParameterNode and ExceptionNode, so it should be used in environments where these types are defined. - -**Output Example**: -A possible appearance of the code's return value from the `get_attrs` method might look like this: -``` -{ - 'label': 'node_name\nnode_description...\nnode_content...', - 'shape': 'ellipse', - 'fillcolor': '', - 'style': 'filled,solid' -} -``` -### FunctionDef __init__(self, style, print_limit) -**__init__**: The function of __init__ is to initialize an instance of the NodeVizStyleGuide class with specific visualization style settings and a print limit. - -**parameters**: The parameters of this Function. -· style: A string parameter that sets the visualization style. The default value is 'default'. -· print_limit: An integer parameter that sets the limit for print operations. The default value is 100. - -**Code Description**: The __init__ function is a constructor method for the NodeVizStyleGuide class. It initializes the instance with two attributes: `style` and `print_limit`. The `style` attribute is set to the value provided by the `style` parameter, which defaults to 'default' if not specified. The `print_limit` attribute is set to the value provided by the `print_limit` parameter, which defaults to 100 if not specified. These attributes are used to configure the visualization style and the print limit for the node visualization guide. - -**Note**: Ensure that the `style` parameter is a valid string representing a visualization style and that the `print_limit` parameter is a positive integer to avoid potential issues during the usage of the NodeVizStyleGuide class. -*** -### FunctionDef get_attrs(self, x) -**get_attrs**: The function of get_attrs is to generate a dictionary of attributes for a node object. - -**parameters**: -- self: Refers to the instance of the class that contains this method. -- x: The node object for which the attributes are generated. - -**Code Description**: -The `get_attrs` function is a method of the `NodeVizStyleGuide` class. It takes a node object `x` as input and generates a dictionary of attributes for the node. The attributes include the label, shape, fill color, and style of the node. - -The function first calls the `get_label` method of the `NodeVizStyleGuide` class to generate the label attribute. It then calls the `get_node_shape` method to determine the shape attribute based on the type of the node. The `get_color` method is called to determine the fill color attribute based on the type of the node. Finally, the `get_style` method is called to determine the style attribute based on the trainable status of the node. - -The function constructs a dictionary `attrs` with the label, shape, fill color, and style attributes, and returns it. - -This function is called by the `backward` method of the `Node` class in the same module. The `backward` method performs a backward pass in a computational graph and utilizes the `get_attrs` function to generate the attributes for each node in the graph. - -**Note**: -- The `get_attrs` function assumes that the `get_label`, `get_node_shape`, `get_color`, and `get_style` methods are implemented correctly and return valid values. -- The function does not handle cases where the node object does not have the required attributes or methods. - -**Output Example**: -If the label of the node is "Node1", the shape is "ellipse", the fill color is "lightgray", and the style is an empty string, the function will return the following dictionary: -``` -{ - 'label': 'Node1', - 'shape': 'ellipse', - 'fillcolor': 'lightgray', - 'style': '' -} -``` -*** -### FunctionDef get_label(self, x) -**get_label**: The function of get_label is to generate a label for a node object. - -**parameters**: -- self: Refers to the instance of the class that contains this method. -- x: The node object for which the label is generated. - -**Code Description**: -The `get_label` function is a method of the `NodeVizStyleGuide` class. It takes a node object `x` as input and generates a label for the node. The label consists of the node's name and description, as well as additional content if available. - -The function first retrieves the description of the node by calling the `description` method of the node object. It then checks if the length of the description exceeds the `print_limit` attribute of the `NodeVizStyleGuide` instance. If it does, the description is truncated and an ellipsis is appended. - -Next, the function constructs the text part of the label by concatenating the node's name and the truncated description. The content of the node is retrieved by accessing the `data` attribute of the node object. If the content is a dictionary and it contains a key named "content", the value associated with that key is used as the content. Otherwise, the content is converted to a string representation. - -Similar to the description, the content is checked against the `print_limit` attribute and truncated if necessary. - -Finally, the function returns the concatenated text and content as the label for the node. - -This function is called by the `get_attrs` method of the `NodeVizStyleGuide` class. The `get_attrs` method generates a dictionary of attributes for a node, including the label, shape, fill color, and style. The `get_label` function is responsible for generating the label attribute of the dictionary. - -**Note**: -- The `get_label` function assumes that the `description` and `data` attributes of the node object are already set and contain valid values. -- The `print_limit` attribute of the `NodeVizStyleGuide` instance determines the maximum length of the description and content before truncation. -- The function does not handle cases where the `data` attribute is not present or is of an unsupported type. - -**Output Example**: -If the name of the node is "Node1" and the description is "This is a sample node description.", the content is a dictionary with the key "content" and value "Sample content", and the `print_limit` is set to 20, the function will return the following label: -``` -Node1 -This is a sample no... -Sample content -``` -*** -### FunctionDef get_node_shape(self, x) -**get_node_shape**: The function of get_node_shape is to determine the shape of a node based on its type. - -**parameters**: The parameters of this Function. -· x: The node whose shape is to be determined. - -**Code Description**: The get_node_shape function is a method designed to return the shape of a node in a computational graph visualization. It takes a single parameter, x, which represents the node whose shape needs to be determined. The function checks the type of the node x. If x is an instance of the ParameterNode class, the function returns the string 'box', indicating that the node should be visualized as a box. For all other types of nodes, the function returns the string 'ellipse', indicating that the node should be visualized as an ellipse. - -This function is utilized within the get_attrs method of the NodeVizStyleGuide class. The get_attrs method calls get_node_shape to include the shape attribute in the dictionary of attributes for a node. This dictionary is used to define various visual properties of the node, such as its label, shape, fill color, and style. - -**Note**: -- The function relies on the type of the node to determine its shape. It specifically checks if the node is an instance of ParameterNode. -- The ParameterNode class represents a trainable node in a computational graph and has various attributes such as value, name, trainable, description, constraint, and info. - -**Output Example**: -- If x is an instance of ParameterNode, the function returns 'box'. -- If x is not an instance of ParameterNode, the function returns 'ellipse'. -*** -### FunctionDef get_color(self, x) -**get_color**: The function of get_color is to determine the color representation of a node based on its type. - -**parameters**: The parameters of this Function. -· x: The node whose color representation is to be determined. - -**Code Description**: The get_color function is a method designed to return a specific color string based on the type of the node passed as an argument. It takes a single parameter, x, which represents the node. The function checks the type of the node and returns a corresponding color string: - -- If the node is of type ExceptionNode, the function returns the color 'firebrick1'. -- If the node is of type ParameterNode, the function returns the color 'lightgray'. -- For any other type of node, the function returns an empty string. - -This function is utilized within the get_attrs method of the NodeVizStyleGuide class. The get_attrs method calls get_color to determine the fill color attribute of a node, which is part of a set of attributes used for visualizing the node. The get_attrs method constructs a dictionary of attributes including label, shape, fill color, and style, where the fill color is obtained by invoking get_color. - -**Note**: -- The function relies on the specific types of nodes (ExceptionNode and ParameterNode) to determine the color. If additional node types need to be supported, the function should be extended accordingly. -- The function returns an empty string for node types that are not explicitly handled, which may need to be addressed depending on the visualization requirements. - -**Output Example**: -- For an ExceptionNode, the function would return 'firebrick1'. -- For a ParameterNode, the function would return 'lightgray'. -- For any other node type, the function would return an empty string. -*** -### FunctionDef get_style(self, x) -**get_style**: The function of get_style is to determine the style attributes of a node based on its trainable status. - -**parameters**: The parameters of this Function. -· x: An object that contains the attribute 'trainable'. - -**Code Description**: The get_style function evaluates the 'trainable' attribute of the input object 'x'. If 'x.trainable' is True, the function returns the string 'filled,solid', indicating that the node should be styled with a filled and solid appearance. If 'x.trainable' is False, the function returns an empty string, indicating that no specific style should be applied. - -This function is called by the get_attrs function within the same module. The get_attrs function constructs a dictionary of attributes for a node, including its label, shape, fill color, and style. The get_style function specifically provides the 'style' attribute for this dictionary, ensuring that nodes which are trainable are visually distinguished by a filled and solid style. - -**Note**: Ensure that the input object 'x' has a 'trainable' attribute; otherwise, the function may raise an AttributeError. - -**Output Example**: -- If x.trainable is True, the return value will be 'filled,solid'. -- If x.trainable is False, the return value will be an empty string "". -*** -## ClassDef Node -An unknown error occurred while generating this documentation after many tries. -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize a Node object in a computational graph. - -**parameters**: The parameters of this Function. -· value: The initial value of the node. -· name: An optional string representing the name of the node. -· trainable: A boolean indicating whether the node is trainable. -· description: A string providing a description of the node. -· constraint: An optional string representing any constraints on the node. -· info: An optional dictionary containing additional information about the node. - -**Code Description**: The __init__ function initializes a Node object with several attributes. It first calls the superclass initializer with the value and name parameters. The trainable attribute is set based on the provided argument, indicating whether the node can be trained. The _feedback attribute is initialized as a defaultdict of lists, which will store feedback from child nodes. This feedback mechanism is analogous to gradients in machine learning and is used to propagate information back through the graph. The _description attribute stores a textual description of the node, while the _constraint attribute holds any constraints that apply to the node. The _backwarded attribute is a boolean flag indicating whether the backward pass has been called on this node. The _info attribute is a dictionary for storing additional information about the node. Finally, the _dependencies attribute is a dictionary that tracks dependencies on parameters and expandable nodes, which are nodes that depend on parameters not visible at the current graph level. - -**Note**: Points to note about the use of the code -- Ensure that the value parameter is provided when initializing the Node. -- The name parameter is optional but can be useful for identifying nodes in the graph. -- The trainable parameter should be set to True if the node is intended to be updated during training. -- The description, constraint, and info parameters provide additional context and constraints for the node, which can be useful for debugging and documentation purposes. -- The feedback mechanism is designed to support non-commutative aggregation, so feedback should be handled carefully to maintain the correct order of operations. -*** -### FunctionDef zero_feedback(self) -**zero_feedback**: The function of zero_feedback is to reset the feedback attribute of the Node object to an empty state. - -**parameters**: This function does not take any parameters. - -**Code Description**: The zero_feedback function is designed to reset the feedback mechanism of a Node object. It achieves this by setting the _feedback attribute to a new defaultdict with lists as the default factory. This ensures that any previous feedback data stored in the _feedback attribute is cleared, effectively resetting it to an empty state. - -In the context of its usage within the project, the zero_feedback function is called by the backward method of the Node class. During the backward pass, feedback is propagated from the current node to its parent nodes. After this propagation, zero_feedback is invoked to clear the feedback of the current node. This is crucial to prevent the feedback from being double-counted if the retain_graph parameter is set to True. By resetting the feedback, the function ensures that each node's feedback is only considered once during the backward pass, maintaining the integrity of the feedback propagation process. - -**Note**: It is important to note that zero_feedback should be used judiciously within the feedback propagation process to avoid unintended loss of feedback data. It is specifically designed to be used after feedback has been successfully propagated to parent nodes. -*** -### FunctionDef feedback(self) -**feedback**: The function of feedback is to return the internal feedback attribute of the Node object. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The feedback function is a method of the Node class that simply returns the value of the private attribute _feedback. This method does not take any parameters and provides a way to access the internal feedback data stored within the Node object. - -The feedback method is utilized in various parts of the project to retrieve feedback information from Node objects. For instance, in the summarize method of the FunctionOptimizer class, the feedback method is called on each trainable node to aggregate feedback from all parameters. This aggregated feedback is then used to construct a summary of the feedback for further processing. - -Similarly, in the _propagate method of the GraphPropagator class, the feedback method is called on a child node to obtain its feedback, which is then aggregated and propagated to its parent nodes. This ensures that feedback information flows correctly through the graph structure. - -In the AbstractPropagator class, the __call__ method also makes use of the feedback method to propagate feedback from a child node to its parents. This method ensures that the feedback is in the correct format and that all parent nodes receive the appropriate feedback. - -The SumPropagator class's _propagate method uses the feedback method to retrieve user feedback or sum the feedback from various sources, ensuring that the feedback is correctly propagated to parent nodes. - -**Note**: The feedback method is a straightforward accessor method and does not perform any modifications to the internal state of the Node object. It is essential to ensure that the _feedback attribute is correctly initialized and maintained within the Node class to provide accurate feedback information. - -**Output Example**: A possible appearance of the code's return value could be: -``` -{ - "loss": 0.25, - "accuracy": 0.95 -} -``` -This example assumes that the _feedback attribute contains a dictionary with keys representing different metrics and their corresponding values. The actual structure and content of the feedback will depend on the specific implementation and use case within the project. -*** -### FunctionDef description(self) -**description**: The function of description is to return a textual description of the node. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The description function is a method that returns the value of the private attribute `_description` of the Node object. This function is straightforward and does not take any parameters. It simply accesses and returns the `_description` attribute, which is expected to hold a textual description of the node. - -This function is utilized in various parts of the project to retrieve the description of a node. For instance, in the `get_label` method of the `NodeVizStyleGuide` class, the `description` function is called to obtain the node's description, which is then used to generate a label for visualization purposes. The method ensures that the description does not exceed a certain length by truncating it if necessary. - -Similarly, in the `propagate` method of the `Propagator` class, the `description` function is used to get the node's description, which is then processed to determine the appropriate propagation behavior based on the operator name derived from the description. - -**Note**: This function assumes that the `_description` attribute is already set and contains a valid string. It does not perform any validation or modification of the description. - -**Output Example**: -If the `_description` attribute of a Node object is set to "This is a sample node description.", calling the `description` function will return: -"This is a sample node description." -*** -### FunctionDef info(self) -**info**: The function of info is to return the value of the `_info` attribute of the object. - -**parameters**: -- self: The object itself. - -**Code Description**: -The `info` function is a method of the `Node` class. It returns the value of the `_info` attribute of the object. The `_info` attribute is a private attribute that stores additional information about the node. - -The purpose of the `info` function is to provide access to the `_info` attribute, allowing users to retrieve any additional information associated with the node. - -This function does not take any arguments other than `self`, which refers to the object itself. By calling `info()` on a `Node` object, the function will return the value of the `_info` attribute. - -The `_info` attribute can be set by the user or by other functions within the code. It is typically used to store metadata or any other relevant information about the node. - -**Note**: -- The `info` function is a simple getter method that provides access to the `_info` attribute of the object. -- The `_info` attribute can be accessed directly, but it is recommended to use the `info` function for consistency and encapsulation. - -**Output Example**: -If the `_info` attribute of the object is set to `"This is a node"`, calling `info()` will return `"This is a node"`. -*** -### FunctionDef parameter_dependencies(self) -**parameter_dependencies**: The function of parameter_dependencies is to return the dependencies related to parameters within the Node object. - -**parameters**: This function does not take any parameters. - -**Code Description**: The parameter_dependencies function is a method within the Node class that retrieves and returns the parameter dependencies stored in the Node object. Specifically, it accesses the '_dependencies' attribute of the Node instance, which is a dictionary, and returns the value associated with the 'parameter' key. This value represents the set of dependencies that are related to the parameters of the Node. - -The function is utilized by the external_dependencies method in the MessageNode class. In this context, the external_dependencies method checks if the 'info' attribute of the MessageNode instance is a dictionary and if it contains an 'output' key that is an instance of Node. It then compares the length of the parameter dependencies of the 'output' Node with the parameter dependencies of the current MessageNode. If the 'output' Node has more parameter dependencies, it returns the difference between the two sets of dependencies. This indicates that the external_dependencies method relies on the parameter_dependencies function to determine the parameter dependencies of the Node instances it interacts with. - -**Note**: Ensure that the '_dependencies' attribute is properly initialized and contains a 'parameter' key with a corresponding value before calling the parameter_dependencies function to avoid potential KeyError exceptions. - -**Output Example**: A possible return value of the parameter_dependencies function could be a set of dependencies, such as: -``` -{'dependency1', 'dependency2', 'dependency3'} -``` -*** -### FunctionDef expandable_dependencies(self) -**expandable_dependencies**: The function of expandable_dependencies is to retrieve the 'expandable' dependencies from the Node object's internal dependencies dictionary. - -**parameters**: This function does not take any parameters. - -**Code Description**: The expandable_dependencies function is a method of the Node class. It accesses the Node object's internal dictionary, `_dependencies`, and returns the value associated with the key 'expandable'. This dictionary is assumed to store various types of dependencies, and the 'expandable' key specifically holds the dependencies that can be expanded. The function provides a straightforward way to access these expandable dependencies without directly interacting with the internal dictionary. - -**Note**: -- Ensure that the '_dependencies' dictionary is properly initialized and contains the 'expandable' key before calling this function to avoid potential KeyError exceptions. -- This function assumes that the 'expandable' key in the '_dependencies' dictionary holds a valid value that can be returned. - -**Output Example**: -If the '_dependencies' dictionary is structured as follows: -```python -self._dependencies = { - 'expandable': ['dependency1', 'dependency2'], - 'non_expandable': ['dependency3'] -} -``` -Calling `expandable_dependencies()` would return: -```python -['dependency1', 'dependency2'] -``` -*** -### FunctionDef _add_feedback(self, child, feedback) -**_add_feedback**: The function of _add_feedback is to add feedback from a child node to the current node. - -**parameters**: The parameters of this Function. -· child: The child node from which the feedback is received. -· feedback: The feedback data to be added. - -**Code Description**: The _add_feedback function is designed to manage feedback propagation in a node-based structure. It takes two parameters: 'child', which represents the child node providing the feedback, and 'feedback', which is the actual feedback data to be appended. The function appends the feedback to a list associated with the child node in the _feedback dictionary of the current node. - -In the context of its usage within the backward function, _add_feedback plays a crucial role in the feedback propagation mechanism. During the backward pass, feedback is propagated from child nodes to parent nodes. The backward function initializes the feedback for the current node and then propagates it to its parents. The _add_feedback function is called to append the propagated feedback from a child node to the current node's feedback list. This ensures that each node accumulates feedback from its children, which can then be used for further processing or analysis. - -**Note**: Points to note about the use of the code -- Ensure that the _feedback dictionary is properly initialized and that each child node has an associated list to append feedback to. -- The function assumes that the child node is already present in the _feedback dictionary. -- Proper handling of feedback data is essential to avoid issues during the feedback propagation process. -*** -### FunctionDef _set(self, value) -**_set**: The function of _set is to set the value of the node. If the value is a Node, it will be unwrapped. - -**parameters**: The parameters of this Function. -· value: The value to be set for the node. It can be of any type, including another Node. - -**Code Description**: The _set function is designed to assign a value to the node's internal data attribute. It first checks if the provided value is an instance of the Node class. If it is, the function retrieves the internal data of the Node by accessing its data attribute, effectively unwrapping the Node. This ensures that the node's internal data is set to the actual data contained within the provided Node, rather than the Node object itself. If the value is not a Node, it is directly assigned to the node's internal data attribute. This function is crucial for maintaining the integrity of the node's data, especially when dealing with nested Node objects. - -**Note**: This function assumes that the "_data" attribute exists within the node object. If this attribute is not present, an AttributeError will be raised. -*** -### FunctionDef backward(self, feedback, propagator, retain_graph, visualize, simple_visualization, reverse_plot, print_limit) -**backward**: The `backward` function is responsible for performing a backward pass in a computational graph. It propagates feedback from the current node to its parents, updates the graph visualization if required, and returns the resulting graph. - -**parameters**: -- `feedback`: An optional parameter that represents the feedback given to the current node. It can be of any type. -- `propagator`: An optional parameter that represents a function used to propagate feedback from a node to its parents. If not provided, a default `GraphPropagator` object is used. -- `retain_graph`: A boolean parameter that determines whether to retain the graph after the backward pass. If set to `True`, the graph will be retained; otherwise, it will be cleared. The default value is `False`. -- `visualize`: A boolean parameter that determines whether to plot the graph using graphviz. If set to `True`, the graph will be visualized; otherwise, it will not be plotted. The default value is `False`. -- `simple_visualization`: A boolean parameter that determines whether to simplify the visualization by bypassing chains of identity operators. If set to `True`, identity operators will be skipped in the visualization; otherwise, they will be included. The default value is `True`. -- `reverse_plot`: A boolean parameter that determines the order of the graph visualization. If set to `True`, the graph will be plotted in reverse order (from child to parent); otherwise, it will be plotted in the default order (from parent to child). The default value is `False`. -- `print_limit`: An integer parameter that sets the maximum number of characters to print in the graph visualization. If the description or content of a node exceeds this limit, it will be truncated. The default value is `100`. - -**Code Description**: -The `backward` function is a method of the current object. It performs a backward pass in a computational graph by propagating feedback from the current node to its parents. The function takes several parameters to control the behavior of the backward pass. - -The `feedback` parameter represents the feedback given to the current node. It can be of any type and is used to initialize the feedback mechanism of the node. The `propagator` parameter is an optional function that is used to propagate feedback from a node to its parents. If not provided, a default `GraphPropagator` object is used, which implements specific methods for feedback propagation. The `retain_graph` parameter determines whether to retain the graph after the backward pass. If set to `True`, the graph will be retained; otherwise, it will be cleared. The `visualize` parameter determines whether to plot the graph using graphviz. If set to `True`, the graph will be visualized; otherwise, it will not be plotted. The `simple_visualization` parameter determines whether to simplify the visualization by bypassing chains of identity operators. If set to `True`, identity operators will be skipped in the visualization; otherwise, they will be included. The `reverse_plot` parameter determines the order of the graph visualization. If set to `True`, the graph will be plotted in reverse order (from child to parent); otherwise, it will be plotted in the default order (from parent to child). The `print_limit` parameter sets the maximum number of characters to print in the graph visualization. If the description or content of a node exceeds this limit, it will be truncated. - -The function first checks if a `propagator` object is provided. If not, it imports the `GraphPropagator` class from the `opto.trace.propagators.graph_propagator` module. It then initializes the `propagator` object if it is not provided. - -Next, the function sets up the visualization by creating a `digraph` object and a `NodeVizStyleGuide` object. These objects are used to plot the graph using graphviz and define the style of the nodes in the graph. - -The function checks if the current node has already been backwarded. If it has, an `AttributeError` is raised. Otherwise, the function adds the feedback to the current node by calling the `_add_feedback` method of the node object. The feedback is initialized with a special "FEEDBACK_ORACLE" node and the propagated feedback from the `propagator` object. - -If the current node has no parents, indicating that it is a root node, the function checks if visualization is enabled. If it is, the current node is added to the `digraph` object with the appropriate style attributes. Finally, the function returns the `digraph` object. - -If the current node has parents, indicating that it is not a root node, the function initializes a priority queue called `queue` using the `MinHeap` class. The priority queue is used to process the nodes in the correct order during the backward pass. - -The function enters a loop that continues until the `queue` is empty. In each iteration, a node is popped from the `queue` and processed. The node is checked to ensure it has parents and is an instance of the `MessageNode` class. If not, an `AttributeError` is raised. - -The function propagates information from the current node to its parents by calling the `propagator` object with the current node as the argument. The `propagator` object computes the propagated feedback based on the child node's description, data, and feedback. The propagated feedback is then added to the parents of the current node by calling the `_add_feedback` method of each parent node. - -The function checks if visualization is enabled. If it is, the function plots the edge from each parent to the current node in the `digraph` object. It also handles the visualization of identity operators by bypassing chains of identity operators if the `simple_visualization` parameter is set to `True`. - -After processing the parents of the current node, the `_backwarded` attribute of the current node is updated to indicate that it has been backwarded. This attribute is set to `True` unless the `retain_graph` parameter is set to `True`. - -The loop continues until the `queue` is empty, indicating that all the nodes have been processed. Finally, the function returns the `digraph` object. - -**Note**: -- The `backward` function is a crucial part of the backward pass in a computational graph. It propagates feedback from child nodes to parent nodes, updates the graph visualization if required, and returns the resulting graph. -- The `feedback` parameter is used to initialize the feedback mechanism of the current node. It can be of any type and is specific to the application. -- The `propagator` parameter allows for customization of the feedback propagation process. If not provided, a default `GraphPropagator` object is used. -- The `retain_graph` parameter determines whether to retain the graph after the backward pass. This can be useful for further analysis or visualization. -- The `visualize` parameter allows for visualization of the graph using graphviz. This can be helpful for understanding the structure of the graph. -- The `simple_visualization` parameter simplifies the visualization by bypassing chains of identity operators. This can improve the clarity of the graph. -- The `reverse_plot` parameter determines the order of the graph visualization. This can be useful for visualizing the graph from child to parent, which may be more intuitive in some cases. -- The `print_limit` parameter sets a limit on the number of characters to print in the graph visualization. This can prevent the visualization from becoming too cluttered or overwhelming. - -**Output Example**: -If the current node has two parents and visualization is enabled, the `backward` function will return a `digraph` object representing the graph with the appropriate edges and node styles. -*** -### FunctionDef clone(self) -**clone**: The function of clone is to create and return a duplicate of the current Node object. - -**parameters**: The parameters of this Function. -· This function does not take any parameters other than the implicit self parameter, which refers to the instance of the Node class. - -**Code Description**: The clone function is a method of the Node class that imports the clone function from the opto.trace.operators module and applies it to the current instance (self) of the Node class. The imported clone function from the operators module is responsible for creating a duplicate of the Node instance. This method ensures that the Node object can be cloned using a standardized operation defined in the operators module. - -The clone function is also indirectly referenced by the identity function in the opto.trace.operators module. The identity function calls the clone method on its input parameter, effectively creating a duplicate of the input object. This demonstrates that the clone method is integral to operations that require object duplication within the project. - -**Note**: -- Ensure that the opto.trace.operators module is correctly imported and accessible when using the clone method. -- The clone method does not modify the original Node object; it only creates and returns a duplicate. - -**Output Example**: The return value of the clone function will be a new instance of the Node class that is a duplicate of the original instance. For example, if the original Node instance has specific attributes and states, the cloned instance will have the same attributes and states. -*** -### FunctionDef detach(self) -**detach**: The function of detach is to create and return a deep copy of the current instance of the Node class. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The detach function is designed to create a deep copy of the current instance of the Node class. When this function is called, it utilizes the deepcopy method from the copy module to generate a new instance of the Node class that is a complete copy of the original, including all nested objects. This ensures that any changes made to the new instance do not affect the original instance, and vice versa. The function then returns this new deep-copied instance. - -**Note**: -- Ensure that the copy module is imported before using this function. -- This function does not modify the original instance; it only creates and returns a new deep-copied instance. - -**Output Example**: -If the original instance of the Node class has certain attributes and nested objects, calling the detach function will return a new instance with identical attributes and nested objects, but completely independent of the original instance. For example: - -```python -original_node = Node() -detached_node = original_node.detach() -# detached_node is a deep copy of original_node -``` -*** -### FunctionDef getattr(self, key) -**getattr**: The function of getattr is to get the value of the specified attribute from the given object. - -**parameters**: -- self: The object from which the attribute value is to be retrieved. -- key: A string representing the name of the attribute to be retrieved. - -**Code Description**: -The `getattr` function is a method of the `Node` class in the `opto.trace.nodes.py` module. It takes in the `self` object, which is an instance of the `Node` class, and a string `key` as parameters. - -The function first imports the `node_getattr` function from the `opto.trace.operators` module. It then calls the `node_getattr` function passing itself (`self`) and the specified attribute (`key`) as arguments. The `node_getattr` function is responsible for retrieving the value of the specified attribute from the `Node` object. - -The `getattr` method is used to access the attributes of the `Node` object. It is called when the `getattr` function is invoked on a `Node` object. The `getattr` method retrieves the value of the specified attribute from the `Node` object by calling the `node_getattr` function. - -**Note**: -- The `getattr` method assumes that the `self` parameter is a valid `Node` object. -- If the `self` object does not have the specified attribute, a `AttributeError` will be raised. - -**Output Example**: -A possible return value of the `getattr` method could be the value of the specified attribute from the `Node` object. -*** -### FunctionDef call(self, fun) -**call**: The function of call is to invoke a specified function with the given arguments and keyword arguments. - -**parameters**: -- self: The object on which the function is called. -- fun: A string representing the name of the function to be invoked. -- *args: Variable-length positional arguments to be passed to the function. -- **kwargs: Variable-length keyword arguments to be passed to the function. - -**Code Description**: -The `call` function is a method of the `Node` class in the `opto.trace.nodes.py` module. It takes in the `self` object, which is an instance of the `Node` class, a string `fun`, and variable-length positional and keyword arguments (`args` and `kwargs`) as parameters. - -The function first iterates over the `args` and converts each argument to a `Node` object using the `node` function. This is done to ensure that all arguments passed to the function are `Node` objects. The converted arguments are then stored in a generator expression. - -Next, the function iterates over the `kwargs` and converts each value to a `Node` object using the `node` function. The converted values are then stored in a dictionary comprehension, with the keys being the original keys from `kwargs`. - -Finally, the function calls the `getattr` method of the `self` object, passing the `fun` string as the attribute name. The `getattr` method retrieves the value of the specified attribute from the `self` object. The retrieved attribute is then invoked as a function, passing the converted `args` and `kwargs` as arguments. - -The `call` method is used to dynamically invoke functions on the `Node` object. It allows for flexible and dynamic function calls based on the provided arguments and keyword arguments. - -**Note**: -- The `fun` parameter should be a string representing the name of a valid function that can be invoked on the `self` object. -- The `args` and `kwargs` parameters can be any valid arguments that can be passed to the specified function. -- The `call` method assumes that the `self` parameter is a valid `Node` object with the specified function as an attribute. - -**Output Example**: A possible return value of the `call` method could be the result of invoking the specified function with the provided arguments and keyword arguments. -*** -### FunctionDef __call__(self) -**__call__**: The function of __call__ is to invoke the `call` function from the `opto.trace.operators` module with the provided arguments and keyword arguments. - -**parameters**: The parameters of this function. -· `*args`: Variable-length argument list. -· `**kwargs`: Keyword arguments. - -**Code Description**: The `__call__` method is designed to facilitate the invocation of a function encapsulated within a Node object. When this method is called, it imports the `call` function from the `opto.trace.operators` module. The `call` function is then executed with the current instance (`self`) and any additional arguments (`*args`) and keyword arguments (`**kwargs`) provided to the `__call__` method. - -The `call` function, as defined in the `opto.trace.operators` module, takes a Node object representing the function to be called, along with any positional and keyword arguments. It ensures that the function encapsulated within the Node object is callable and then invokes it with the provided arguments. The result of this invocation is returned as the output. - -By using the `__call__` method, the Node object can be used as if it were a regular callable function, providing a seamless interface for function invocation. - -**Note**: -- The Node object must encapsulate a callable function. -- The `*args` parameter can accept any number of positional arguments. -- The `**kwargs` parameter can accept any number of keyword arguments. - -**Output Example**: -If the Node object encapsulates a function defined as follows: -```python -def add(a, b): - return a + b -``` -and the `__call__` method is invoked with `args=(2, 3)`, the output will be `5`. -*** -### FunctionDef len(self) -**len**: The function of len is to return the length of the Node instance. - -**parameters**: The parameters of this Function. -· self: The Node instance whose length is to be calculated. - -**Code Description**: The len method is a member of the Node class in the opto.trace.nodes module. This method is designed to compute and return the length of the Node instance. When invoked, the len method imports the len_ function from the opto.trace.operators module and applies it to the Node instance (self). The len_ function is a utility that leverages Python's built-in len() function to determine the length of the input object. By using the len_ function, the len method ensures a consistent and modular approach to length calculation within the project. This design promotes reusability and maintainability, as the len_ function can be utilized across different parts of the project. - -**Note**: Ensure that the Node instance supports the len() operation. Passing an unsupported type will result in a TypeError. - -**Output Example**: -- If the Node instance represents a list [1, 2, 3], len(self) will return 3. -- If the Node instance represents a string "hello", len(self) will return 5. -*** -### FunctionDef __getitem__(self, key) -**__getitem__**: The function of __getitem__ is to retrieve an element from a Node instance using a specified key. - -**parameters**: The parameters of this function. -· key: The key used to access the element within the Node instance. - -**Code Description**: The __getitem__ method is designed to facilitate element retrieval from a Node instance using a specified key. When this method is called, it first imports the getitem function from the opto.trace.operators module. It then uses the node function to create a Node object from the provided key. Finally, it calls the getitem function with the current Node instance (self) and the newly created Node object (from the key) as arguments. This modular approach allows for flexible and reusable element retrieval within the Node class. - -The node function is responsible for creating a Node object from a given message. If the message is already a Node, it returns the message as is. This function simplifies the creation of Node objects and ensures consistency in how Nodes are instantiated. - -The getitem function is a straightforward implementation of the indexing operation. It takes an object and an index as parameters and returns the element located at the specified index within the object. In this context, the getitem function is used to retrieve an element from the Node instance using the key provided to the __getitem__ method. - -**Note**: -- Ensure that the key provided is compatible with the indexing mechanism of the Node instance. -- The node function should be used to create Node objects instead of directly invoking the Node class. - -**Output Example**: If a Node instance contains a list [10, 20, 30] and the key provided is 1, the return value of the __getitem__ method will be 20. -*** -### FunctionDef __contains__(self, item) -**__contains__**: The function of __contains__ is to determine if a given item is part of the Node instance. - -**parameters**: The parameters of this Function. -· item: The element to be checked for presence within the Node instance. - -**Code Description**: The __contains__ method is a special method in Python that allows the use of the `in` operator to check for membership within an object. In this context, the __contains__ method is part of the Node class in the opto\trace\nodes.py module. - -When the __contains__ method is called, it first imports the `in_` function from the opto.trace.operators module. The `in_` function is designed to determine whether an element `x` is present within a collection `y`. - -Next, the __contains__ method converts the `item` into a Node object using the `node` function. The `node` function is responsible for creating a Node object from a given message. If the message is already a Node, it returns the message as is. This ensures that the `item` is always in the form of a Node object before performing the membership test. - -Finally, the __contains__ method calls the `in_` function with the Node-converted `item` and the Node instance (`self`) as arguments. The `in_` function then checks if the `item` is present within the Node instance and returns a boolean value indicating the result. - -**Note**: -- The `item` parameter must be convertible to a Node object using the `node` function. -- The Node instance (`self`) must support the membership test operation. - -**Output Example**: -- If `item` is a Node object that is part of the Node instance, the method will return True. -- If `item` is not part of the Node instance, the method will return False. -*** -### FunctionDef __pos__(self) -**__pos__**: The function of __pos__ is to return the unary positive of the Node instance. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class on which the unary positive operator is applied. - -**Code Description**: The __pos__ method is a special method in Python that is invoked when the unary positive operator (+) is used on an instance of the Node class. When this operator is applied, the __pos__ method is called, which in turn imports the pos function from the opto.trace.operators module. The pos function is then called with the Node instance (self) as its argument. The pos function applies the unary positive operator to the input value and returns it. In this context, the unary positive operator does not alter the value of the Node instance; it simply returns the instance itself. This ensures that the unary positive operation is consistently applied to instances of the Node class. - -**Note**: -- The __pos__ method does not modify the Node instance; it simply returns it. -- Ensure that the Node class instances are of a type that supports the unary positive operator. - -**Output Example**: -If the Node instance is node_instance, the return value will be node_instance when +node_instance is used. -*** -### FunctionDef __neg__(self) -**__neg__**: The function of __neg__ is to return the negation of the Node instance. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class to be negated. - -**Code Description**: The __neg__ method is a special method in Python that is invoked when the unary negation operator (-) is applied to an instance of the Node class. This method imports the neg function from the opto.trace.operators module and applies it to the Node instance (self). The neg function, in turn, returns the negation of its input value using the unary negation operator (-). Therefore, when the __neg__ method is called, it effectively negates the Node object by leveraging the neg function. - -**Note**: Ensure that the Node instance supports the unary negation operator to avoid runtime errors. - -**Output Example**: If the Node instance represents a value of 5, applying the unary negation operator will result in -5. If the Node instance represents a value of -3.2, applying the unary negation operator will result in 3.2. -*** -### FunctionDef __abs__(self) -**__abs__**: The function of __abs__ is to return the absolute value of the Node instance. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class on which the __abs__ method is called. - -**Code Description**: The __abs__ method is a special method in Python that is called when the built-in abs() function is used on an instance of the Node class. When invoked, this method imports the abs function from the opto.trace.operators module and applies it to the Node instance (self). The imported abs function is designed to compute the absolute value of its input, leveraging Python's built-in abs() function. This allows the Node class to utilize the abs function to compute and return the absolute value of its instances. - -**Note**: -- Ensure that the Node instance supports the absolute value operation, either directly or through a custom implementation of the __abs__ method. -- The behavior and limitations of this method are consistent with Python's built-in abs() function. - -**Output Example**: -- If the Node instance represents a value of -5, the __abs__ method will return 5. -- If the Node instance represents a value of 3.14, the __abs__ method will return 3.14. -- If the Node instance is a custom object that implements the __abs__ method, the __abs__ method will return the result of that custom implementation. -*** -### FunctionDef __invert__(self) -**__invert__**: The function of __invert__ is to perform a bitwise NOT operation on the instance of the Node class. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class on which the bitwise NOT operation will be performed. - -**Code Description**: The __invert__ method is a special method in Python that allows the use of the bitwise NOT operator (~) on an instance of the Node class. When the ~ operator is applied to a Node instance, the __invert__ method is invoked. This method imports the invert function from the opto.trace.operators module and applies it to the instance (self). - -The invert function, defined in the opto.trace.operators module, takes a single parameter x and returns the result of applying the bitwise NOT operation to x. The bitwise NOT operation inverts each bit of the input value. For example, if x is an integer, each bit in its binary representation will be flipped (0s become 1s and 1s become 0s). - -In this context, the __invert__ method enables the Node class to support the bitwise NOT operation by leveraging the invert function. This allows developers to use the ~ operator directly on Node instances, making the code more intuitive and concise. - -**Note**: Ensure that the Node instance supports the bitwise NOT operation. Using types that do not support this operation will result in a TypeError. - -**Output Example**: -- If the Node instance represents an integer with a value of 5, the return value will be -6. -- If the Node instance represents an integer with a value of 0, the return value will be -1. -*** -### FunctionDef __round__(self, n) -**__round__**: The function of __round__ is to round the value of the Node object to a specified number of decimal places. - -**parameters**: The parameters of this function. -· n: The number of decimal places to round to. This parameter is optional and can be None. - -**Code Description**: The __round__ method is a special method in the Node class that allows rounding the value of the Node object to a specified number of decimal places. It imports the round function from the opto.trace.operators module and applies it to the Node instance (self). If the parameter n is provided, it is converted into a Node object using the node function from the same module. If n is not provided (i.e., it is None), the round function is called with None as the second argument. - -The method works as follows: -1. It imports the round function from the opto.trace.operators module. -2. It checks if the parameter n is provided. -3. If n is provided, it converts n into a Node object using the node function. -4. It calls the round function with the Node instance (self) and the converted n (or None if n is not provided). -5. It returns the result of the round function. - -The relationship with its callees is as follows: -- The node function is used to convert the parameter n into a Node object if n is provided. -- The round function is used to perform the actual rounding operation on the Node instance. - -**Note**: -- Ensure that the parameter n, if provided, can be interpreted as an integer to avoid runtime errors. -- The method relies on the round function from the opto.trace.operators module, which is a wrapper around Python's built-in round function. - -**Output Example**: -If the Node instance represents the value 3.14159 and n is 2, the method will return a Node object representing the value 3.14. -If the Node instance represents the value 3.14159 and n is 0, the method will return a Node object representing the value 3. -*** -### FunctionDef __floor__(self) -**__floor__**: The function of __floor__ is to compute the largest integer less than or equal to the value of the current Node instance. - -**parameters**: The parameters of this Function. -· self: An instance of the Node class. - -**Code Description**: The __floor__ method is a special method in the Node class that allows instances of Node to be floored directly. When this method is called, it imports the floor function from the opto.trace.operators module and applies it to the current instance (self). The floor function, in turn, computes the largest integer less than or equal to the given number using Python's math.floor method. This operation is useful for rounding down the value of the Node instance to the nearest whole number. - -**Note**: Ensure that the Node instance holds a numeric value that can be floored. If the value is not numeric, the floor function will raise a TypeError. Additionally, the math module must be available in the environment for the floor function to work correctly. - -**Output Example**: -- If the Node instance has a value of 3.7, calling __floor__() will return 3. -- If the Node instance has a value of -2.3, calling __floor__() will return -3. -*** -### FunctionDef __ceil__(self) -**__ceil__**: The function of __ceil__ is to return the smallest integer greater than or equal to the value represented by the Node instance. - -**parameters**: The parameters of this Function. -· self: An instance of the Node class. - -**Code Description**: The __ceil__ method is a special method in the Node class that provides a ceiling operation on the Node instance. When invoked, it imports the ceil function from the opto.trace.operators module and applies it to the Node instance (self). The ceil function, in turn, rounds up the numeric value represented by the Node instance to the nearest integer. This method leverages the functionality of the ceil function to ensure that the Node instance's value is rounded up correctly. - -The ceil function, which is called within __ceil__, is designed to handle any numeric type and uses the math.ceil() method from the math module to perform the rounding operation. By importing and utilizing this function, the __ceil__ method ensures that the Node instance's value is processed accurately and efficiently. - -**Note**: Ensure that the Node instance represents a numeric value; otherwise, the ceil function will raise a TypeError. The math module must be available in the environment where the code is executed. - -**Output Example**: -- If the Node instance represents the value 4.2, __ceil__() will return 5. -- If the Node instance represents the value -3.7, __ceil__() will return -3. -- If the Node instance represents the value 7, __ceil__() will return 7. -*** -### FunctionDef __trunc__(self) -**__trunc__**: The function of __trunc__ is to truncate the decimal part of a Node object, returning its integer part. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class that is to be truncated. - -**Code Description**: The __trunc__ method is a special method in the Node class that allows instances of Node to be truncated to their integer representation. When __trunc__ is called on a Node instance, it imports the trunc function from the opto.trace.operators module and applies it to the instance (self). The trunc function, in turn, utilizes Python's math.trunc function to truncate the decimal part of the number, returning only the integer part. This ensures that any Node object can be converted to its integer form when necessary. - -**Note**: -- The Node instance should be compatible with the math.trunc function, typically meaning it should represent a numerical value. -- If the Node instance does not represent a number, the trunc function will raise a TypeError. - -**Output Example**: -If a Node instance represents the value 3.14, calling __trunc__ on this instance will return 3. -If a Node instance represents the value -2.99, calling __trunc__ on this instance will return -2. -*** -### FunctionDef __add__(self, other) -**__add__**: The function of __add__ is to define the addition operation for Node objects, allowing them to be combined with other values. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The value to be added to the current Node instance. This can be of any type. - -**Code Description**: The __add__ method in the Node class is designed to handle the addition of a Node object with another value. It first imports the necessary operators from the opto.trace.operators module. The method then checks the type of the _data attribute of the Node instance. If _data is a string, it uses the concat function from the operators module to concatenate the current Node instance with another Node instance created from the other parameter. If _data is not a string, it uses the add function from the operators module to add the current Node instance to another Node instance created from the other parameter. - -The node function is used to ensure that the other parameter is converted into a Node object if it is not already one. This function provides a convenient way to create Node objects from various types of messages, ensuring consistency and ease of use. - -The __add__ method is also called by the __radd__ method in the Node class, which allows for the reverse addition operation. This means that if the other parameter is on the left side of the addition operation, the __radd__ method will be invoked, which in turn calls the __add__ method to perform the addition. - -**Note**: -- Ensure that the types of the _data attribute and the other parameter are compatible with the + operator to avoid runtime errors. -- The behavior of the + operator varies depending on the types of the operands. For example, it concatenates strings and lists but adds numbers. - -**Output Example**: -- If self._data is "Hello" and other is "World", the return value will be a Node object with _data "HelloWorld". -- If self._data is 3 and other is 5, the return value will be a Node object with _data 8. -*** -### FunctionDef __radd__(self, other) -**__radd__**: The function of __radd__ is to handle the reverse addition operation for Node objects, allowing them to be combined with other values when the Node instance is on the right side of the addition. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The value to be added to the current Node instance. This can be of any type. - -**Code Description**: The __radd__ method in the Node class is designed to facilitate the addition operation when the Node instance appears on the right side of the addition operator. This method is invoked when the left operand does not support the addition operation with the right operand, which is an instance of the Node class. The __radd__ method simply calls the __add__ method of the Node class, passing the other parameter to it. This ensures that the addition logic defined in the __add__ method is reused, maintaining consistency in how Node objects are combined with other values. - -The __add__ method, which is called by __radd__, handles the addition by checking the type of the _data attribute of the Node instance. If _data is a string, it concatenates the current Node instance with another Node instance created from the other parameter using the concat function from the opto.trace.operators module. If _data is not a string, it adds the current Node instance to another Node instance created from the other parameter using the add function from the same module. The node function ensures that the other parameter is converted into a Node object if it is not already one. - -**Note**: -- Ensure that the types of the _data attribute and the other parameter are compatible with the + operator to avoid runtime errors. -- The behavior of the + operator varies depending on the types of the operands. For example, it concatenates strings and lists but adds numbers. - -**Output Example**: -- If self._data is "Hello" and other is "World", the return value will be a Node object with _data "HelloWorld". -- If self._data is 3 and other is 5, the return value will be a Node object with _data 8. -*** -### FunctionDef __sub__(self, other) -**__sub__**: The function of __sub__ is to perform a subtraction operation between the current Node object and another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The operand to be subtracted from the current Node object. This operand can be any type that can be converted into a Node object. - -**Code Description**: The __sub__ method is designed to enable the use of the subtraction operator (-) between Node objects or between a Node object and another operand. When the subtraction operator is used, this method is invoked. The method first imports the subtract function from the opto.trace.operators module. It then calls the node function from the opto.trace.nodes module to ensure that the operand 'other' is converted into a Node object if it is not already one. Finally, it calls the subtract function with the current Node object (self) and the newly created Node object from the operand 'other'. The subtract function performs the actual subtraction operation and returns the result. - -**Note**: -- Ensure that the operand 'other' is of a type that can be converted into a Node object to avoid runtime errors. -- The node function is used to handle the conversion of the operand into a Node object, providing flexibility in the types of operands that can be used with the subtraction operator. - -**Output Example**: -- If self is a Node object representing the value 10 and other is a Node object representing the value 5, the __sub__ method will return a Node object representing the value 5. -- If self is a Node object representing a list [1, 2, 3] and other is a Node object representing a list [1, 1, 1], the __sub__ method will return a Node object representing the list [0, 1, 2] (assuming the subtraction operation is defined for lists in this context). -*** -### FunctionDef __mul__(self, other) -**__mul__**: The function of __mul__ is to enable the multiplication operation for Node objects using the * operator. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The operand to be multiplied with the current Node instance. This can be any type that is compatible with the multiplication operation. - -**Code Description**: The __mul__ method allows for the multiplication of a Node object with another operand. When the * operator is used with a Node instance, this method is invoked. It imports the multiply function from the opto.trace.operators module and the node function from the opto.trace.nodes module. - -The method first converts the other operand into a Node object using the node function. This ensures that the operand is in a compatible format for the multiplication operation. The node function checks if the operand is already a Node and returns it as is if true. Otherwise, it creates a new Node object from the operand. - -After converting the operand, the method calls the multiply function with the current Node instance (self) and the newly created Node object as arguments. The multiply function performs the multiplication operation and returns the result. - -This design allows for seamless multiplication of Node objects or Node-compatible objects using the * operator, enhancing the flexibility and usability of the Node class. - -**Note**: Ensure that the operand passed to the * operator is compatible with the multiplication operation to avoid runtime errors. If the operand does not support multiplication, a TypeError will be raised. - -**Output Example**: If self is a Node object representing the value 3 and other is 4, the result of self * other will be a Node object representing the value 12. -*** -### FunctionDef __floordiv__(self, other) -**__floordiv__**: The function of __floordiv__ is to perform floor division between a Node object and another operand. - -**parameters**: The parameters of this function. -· self: The Node object on which the floor division operation is invoked. -· other: The operand with which the floor division is to be performed. This can be any type that supports the floor division operation. - -**Code Description**: The __floordiv__ method is a special method in the Node class that enables the use of the floor division operator (//) between a Node object and another operand. When this method is called, it imports the floor_divide function from the opto.trace.operators module and the node function from the opto.trace.nodes module. - -The method first converts the other operand into a Node object using the node function. This ensures that the operand is compatible with the Node class's operations. It then applies the floor_divide function to the Node object (self) and the newly created Node object (other). The floor_divide function performs the floor division operation, which divides the two operands and rounds down the result to the nearest integer. - -This method ensures that the floor division operation is performed correctly and consistently within the project's framework by leveraging the floor_divide function. The use of the node function guarantees that the other operand is appropriately handled as a Node object, maintaining the integrity of the Node class's operations. - -**Note**: Ensure that the other operand is of a type that supports the floor division operation to avoid runtime errors. The method relies on the floor_divide function, which does not perform type checking or validation, so improper types may lead to unexpected behavior or exceptions. - -**Output Example**: If self is a Node object representing the value 7 and other is an operand representing the value 3, the method call self // other will return a Node object representing the value 2, as 7 // 3 equals 2. -*** -### FunctionDef __truediv__(self, other) -**__truediv__**: The function of __truediv__ is to perform division between the current Node instance and another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The operand to divide the current Node instance by. This can be any type that supports division. - -**Code Description**: The __truediv__ method is designed to handle the division operation for Node objects. When the division operator (/) is used between a Node instance and another operand, this method is invoked. The method first imports the divide function from the opto.trace.operators module. It then converts the other operand into a Node object using the node function from the opto.trace.nodes module. This ensures that both operands are Node objects, maintaining consistency within the framework. Finally, the method returns the result of the divide function, which performs the actual division operation between the two Node objects. - -**Note**: -- Ensure that the divisor (other) is not zero to avoid a ZeroDivisionError. -- The other operand should be of a type that supports the division operation. -- The node function is used to convert the other operand into a Node object if it is not already one, ensuring compatibility within the Node framework. - -**Output Example**: If the current Node instance represents the value 10 and the other operand represents the value 2, the method will return a Node object representing the value 5.0. -*** -### FunctionDef __mod__(self, other) -**__mod__**: The function of __mod__ is to perform the modulo operation between the current Node object and another value. - -**parameters**: The parameters of this function. -· other: The value to be used as the divisor in the modulo operation. It can be of any type that supports the modulo operation. - -**Code Description**: The __mod__ method is designed to enable the modulo operation between a Node object and another value. When this method is called, it first imports the mod function from the opto.trace.operators module. It then calls the node function to ensure that the other value is converted into a Node object if it is not already one. Finally, it applies the mod function to the current Node object (self) and the converted Node object (node(other)), and returns the result. - -The node function is responsible for creating a Node object from a given message. If the message is already a Node, it returns it as is. This ensures that the other value is always in the form of a Node object before the modulo operation is performed. - -The mod function takes two parameters, x and y, and returns the result of the modulo operation (x % y). This operation finds the remainder when x is divided by y. By integrating the mod function with the __mod__ method, Node objects can seamlessly perform the modulo operation with other values, enhancing their arithmetic capabilities. - -**Note**: Ensure that the other value provided is of a type that supports the modulo operation to avoid runtime errors. - -**Output Example**: If the current Node object represents the value 10 and the other value is 3, the return value will be a Node object representing the value 1. If the current Node object represents the value 20 and the other value is 7, the return value will be a Node object representing the value 6. -*** -### FunctionDef __divmod__(self, other) -**__divmod__**: The function of __divmod__ is to perform the divmod operation on a Node object and another operand, returning the result. - -**parameters**: The parameters of this function. -· self: The Node instance on which the __divmod__ method is called. -· other: The operand to be used in the divmod operation with the Node instance. - -**Code Description**: The __divmod__ method is designed to enable the use of the divmod operation on Node objects within the project. When this method is called, it first imports the divmod function from the opto.trace.operators module and the node function from the opto.trace.nodes module. The method then converts the other operand into a Node object using the node function. This ensures that the divmod operation is performed between two Node objects, maintaining consistency within the project's framework. - -The core functionality of the __divmod__ method is to delegate the actual divmod operation to the divmod function imported from opto.trace.operators. This function takes two parameters, x and y, and applies Python's built-in divmod function to them, returning a tuple containing the quotient and the remainder. By using this approach, the __divmod__ method ensures that the divmod operation can be seamlessly integrated with Node objects, providing a consistent interface for performing division and modulus operations within the project's tracing framework. - -**Note**: Ensure that the other operand is of a type that can be converted into a Node object to avoid runtime errors. The method relies on the node function to handle this conversion, so any constraints or behaviors of the node function will apply here as well. - -**Output Example**: If the Node instance represents the value 10 and the other operand is 3, the return value will be a tuple (3, 1), where 3 is the quotient and 1 is the remainder. -*** -### FunctionDef __pow__(self, other) -**__pow__**: The function of __pow__ is to enable the power operation (exponentiation) on Node objects. - -**parameters**: The parameters of this function. -· self: The Node object on which the power operation is being performed. -· other: The exponent value, which can be of any type that supports the power operation. - -**Code Description**: The __pow__ method allows for the use of the power operator (**) directly on Node objects. When this method is called, it imports the power function from the opto.trace.operators module and applies it to the Node object (self) and the other value (other). - -The method first imports the necessary operators from the opto.trace.operators module. It then calls the power function, passing in the current Node object (self) and the result of the node function applied to the other value. The node function ensures that the other value is converted into a Node object if it is not already one, providing a consistent interface for the power operation. - -This integration allows for intuitive mathematical operations within the project's framework, enabling users to perform exponentiation on Node objects seamlessly. - -**Note**: -- Ensure that the types of self and other are compatible with the power operation to avoid runtime errors. -- The node function is used to convert the other value into a Node object if it is not already one, ensuring consistency in the operation. - -**Output Example**: -If self is a Node object representing the value 2 and other is 3, the function will return a Node object representing the value 8, as 2**3 equals 8. -*** -### FunctionDef __lshift__(self, other) -**__lshift__**: The function of __lshift__ is to perform a left bitwise shift operation on a Node object using another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The operand to be used for the left bitwise shift operation. - -**Code Description**: The __lshift__ method in the Node class is designed to facilitate the left bitwise shift operation using the << operator. When this method is invoked, it imports the lshift function from the opto.trace.operators module and the node function from the same module where the Node class is defined. The method then calls the lshift function, passing the current Node instance (self) and the result of the node function applied to the other operand. - -The node function ensures that the other operand is converted into a Node object if it is not already one. This conversion is crucial for maintaining consistency within the Node class operations. The lshift function then performs the left bitwise shift operation on the two Node objects, self and the converted other operand, and returns the result. - -This method allows instances of the Node class to use the << operator for left bitwise shift operations, leveraging the underlying lshift function to handle the actual bitwise manipulation. - -**Note**: -- Ensure that the other operand is of a type that can be converted into a Node object using the node function. -- The left bitwise shift operation is typically used with integer values, so the operands should support this operation to avoid runtime errors. - -**Output Example**: -If the current Node instance represents the value 4 (binary 100) and the other operand is 2, the method will return a Node object representing the value 16 (binary 10000), as the bits of 4 are shifted left by 2 positions. -*** -### FunctionDef __rshift__(self, other) -**__rshift__**: The function of __rshift__ is to perform a bitwise right shift operation on the current Node instance and another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node class. -· other: The operand to be right-shifted with the current Node instance. - -**Code Description**: The __rshift__ method is a special method in the Node class that facilitates the bitwise right shift operation between the current Node instance (self) and another operand (other). This method first imports the rshift function from the opto.trace.operators module. It then calls this rshift function, passing the current Node instance (self) and the result of the node function applied to the other operand. - -The node function is used to ensure that the other operand is converted into a Node object if it is not already one. This conversion is necessary to maintain consistency and compatibility within the Node class operations. The rshift function, once called, performs the bitwise right shift operation (x >> y) on the two operands. - -**Note**: -- Ensure that the other operand is of a type that supports the right shift operation to avoid runtime errors. -- The node function is used to convert the other operand into a Node object if it is not already one, ensuring compatibility within the Node class operations. - -**Output Example**: If the current Node instance represents the value 8 (binary 1000) and the other operand is 2, the __rshift__ method will return a Node object representing the value 2 (binary 10). -*** -### FunctionDef __and__(self, other) -**__and__**: The function of __and__ is to perform a bitwise AND operation between the current Node object and another operand. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The operand to perform the bitwise AND operation with. This can be any type that supports the bitwise AND operation. - -**Code Description**: The __and__ method is designed to facilitate the bitwise AND operation between a Node object and another operand. When this method is called, it first imports the necessary operators from the `opto.trace.operators` module. Specifically, it imports the `and_` function, which is responsible for executing the bitwise AND operation. - -The method then calls the `node` function from the `opto.trace.nodes` module to ensure that the `other` operand is converted into a Node object if it is not already one. The `node` function is a utility that either returns the operand as a Node object or creates a new Node object from the operand. - -Finally, the `__and__` method applies the `and_` function to the current Node object (`self`) and the converted Node object (`node(other)`). The `and_` function performs the bitwise AND operation and returns the result. - -**Note**: -- Ensure that the `other` operand is of a type that supports the bitwise AND operation to avoid runtime errors. -- The `node` function is used to standardize the operand into a Node object, which simplifies the operation and ensures consistency. - -**Output Example**: If the current Node object represents the value 6 (binary 110) and the `other` operand represents the value 3 (binary 011), the method call `self.__and__(other)` will return a Node object representing the value 2 (binary 010). -*** -### FunctionDef __or__(self, other) -**__or__**: The function of __or__ is to perform a bitwise OR operation between the current Node instance and another Node instance. - -**parameters**: The parameters of this function. -· self: The current Node instance. -· other: Another Node instance or a message that can be converted into a Node. - -**Code Description**: The __or__ method is designed to enable the use of the "|" operator to combine two Node instances using a bitwise OR operation. When the "|" operator is used between two Node instances, this method is invoked. - -1. The method first imports the `or_` function from the `opto.trace.operators` module. -2. It then calls the `node` function to ensure that the `other` parameter is converted into a Node instance if it is not already one. -3. Finally, it applies the `or_` function to the current Node instance (`self`) and the converted Node instance (`other`), returning the result. - -The `node` function is responsible for creating a Node object from a message, ensuring that the `other` parameter is in the correct format for the bitwise OR operation. The `or_` function performs the actual bitwise OR operation between the two Node instances. - -**Note**: Ensure that the `other` parameter can be converted into a Node instance to avoid errors. The `or_` function expects both operands to support the bitwise OR operation. - -**Output Example**: If `self` is a Node instance representing the binary value 0101 and `other` is a Node instance representing the binary value 0011, the return value of `self | other` would be a Node instance representing the binary value 0111. -*** -### FunctionDef __xor__(self, other) -**__xor__**: The function of __xor__ is to perform a bitwise XOR operation between the current Node instance and another Node instance or value. - -**parameters**: The parameters of this function. -· self: The current Node instance. -· other: Another Node instance or value to perform the XOR operation with. - -**Code Description**: The __xor__ method is designed to enable the use of the ^ operator to perform a bitwise XOR operation between Node objects. This method imports the xor function from the opto.trace.operators module and applies it to the current Node instance (self) and another Node instance or value (other). - -The method first imports the necessary operators from the opto.trace.operators module. It then calls the xor function, passing in the current Node instance (self) and the result of the node function applied to the other parameter. The node function ensures that the other parameter is converted into a Node object if it is not already one. This allows for seamless integration and operation between Node objects and other values. - -The xor function itself performs the bitwise XOR operation, which compares each bit of its operands and returns 1 if the bits are different, and 0 if they are the same. This operation is useful in various scenarios, such as cryptography, error detection, and correction algorithms. - -**Note**: Ensure that the other parameter is of a type that supports the bitwise XOR operation, such as integers or objects that implement the __xor__ method. The node function will handle the conversion of the other parameter to a Node object if necessary. - -**Output Example**: If the current Node instance represents the value 5 (binary 0101) and the other parameter represents the value 3 (binary 0011), the result of the __xor__ method would be a Node object representing the value 6 (binary 0110). -*** -### FunctionDef __iter__(self) -**__iter__**: The function of __iter__ is to provide an iterable interface for the Node object, allowing it to be iterated over in a consistent manner. - -**parameters**: This function does not take any parameters. - -**Code Description**: The __iter__ method is designed to make the Node object iterable. When called, it imports the iterate function from the opto.trace.containers module. The iterate function is then invoked with the Node object (self) as its argument. The iterate function determines the appropriate iterable class to use based on the type of the Node object's data attribute. It handles various types of collections such as lists, tuples, sets, and dictionaries, and returns an iterable object accordingly. This ensures that the Node object can be iterated over seamlessly, regardless of the type of its data attribute. - -**Note**: -- The Node object must have a data attribute that is a list, tuple, set, or dictionary. -- The iterate function handles the conversion of sets to lists and wraps items in lists or dictionaries with node objects. - -**Output Example**: -If the Node object's data attribute is a list [1, 2, 3], iterating over the Node object would yield: -``` -node(1) -node(2) -node(3) -``` -If the Node object's data attribute is a dictionary {'a': 1, 'b': 2}, iterating over the Node object would yield: -``` -(node('a'), 1) -(node('b'), 2) -``` -*** -### FunctionDef __len__(self) -**__len__**: The function of __len__ is to return the number of elements contained in the Node object. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class. - -**Code Description**: The __len__ method is a special method in Python that is used to define the behavior of the len() function for instances of a class. In this implementation, the __len__ method returns the length of the internal data structure, self._data, which is assumed to be a collection such as a list, dictionary, or any other iterable. The method ensures that the return type is an integer, which is a requirement for the __len__ method in Python. This method provides a straightforward way to get the size of the Node's data without directly accessing the internal data structure. - -**Note**: -- The __len__ method strictly returns an integer value representing the number of elements in the Node's internal data structure. -- If users need a Node object representing the length, they should use a different method, such as node.len(), instead of __len__. - -**Output Example**: -If the Node's internal data structure, self._data, contains 5 elements, calling len(node_instance) will return: -5 -*** -### FunctionDef __lt__(self, other) -**__lt__**: The function of __lt__ is to define the behavior of the less-than operator (<) for Node objects. - -**parameters**: The parameters of this function. -· self: The instance of the Node object on the left-hand side of the < operator. -· other: The object on the right-hand side of the < operator, which can be another Node or a value that can be converted into a Node. - -**Code Description**: The __lt__ method is a special method in Python that allows objects to implement behavior for the less-than operator (<). In this implementation, the method first imports the necessary operators from the opto.trace.operators module. It then calls the lt function from the operators module, passing in the current Node instance (self) and the result of converting the other object into a Node using the node function. - -The node function is responsible for creating a Node object from the other parameter. If the other parameter is already a Node, it is returned as is. Otherwise, a new Node object is created from the other parameter. This ensures that the lt function always receives Node objects as its arguments. - -The lt function from the operators module performs the actual comparison between the two Node objects and returns the result. - -**Note**: -- The __lt__ method relies on the node function to ensure that the other parameter is converted into a Node object if it is not already one. -- The comparison logic is delegated to the lt function from the opto.trace.operators module. - -**Output Example**: A possible return value of the __lt__ method could be a boolean value, such as True or False, indicating whether the current Node instance is less than the other Node instance or value. -*** -### FunctionDef __le__(self, other) -**__le__**: The function of __le__ is to define the behavior of the "less than or equal to" (<=) comparison operator for Node objects. - -**parameters**: The parameters of this function. -· self: The instance of the Node object on the left-hand side of the <= operator. -· other: The object on the right-hand side of the <= operator, which can be another Node or a value that can be converted into a Node. - -**Code Description**: The __le__ function is a special method in Python that allows the use of the <= operator with Node objects. When the <= operator is used, this method is called with the Node instance (self) and the other object (other) being compared. - -1. The function imports the operators module from the opto.trace package as ops. -2. It then calls the le function from the ops module, passing in the current Node instance (self) and the result of the node function applied to the other object. - -The node function is used to ensure that the other object is converted into a Node if it is not already one. This conversion is necessary because the le function in the ops module expects both arguments to be Node objects. - -The le function in the ops module performs the actual comparison between the two Node objects and returns the result. - -**Note**: -- The __le__ method ensures that comparisons using the <= operator are consistent and meaningful for Node objects. -- The node function is used to handle the conversion of the other object to a Node, ensuring compatibility with the le function in the ops module. - -**Output Example**: A possible return value of the __le__ function could be a boolean value, such as True or False, indicating whether the left-hand side Node is less than or equal to the right-hand side Node. -*** -### FunctionDef __gt__(self, other) -**__gt__**: The function of __gt__ is to compare if the current Node object is greater than another object. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The object to compare with the current Node instance. - -**Code Description**: The __gt__ method is a special method in Python used to define the behavior of the greater-than operator (>) for instances of a class. In this implementation, the method first imports the operators module from the opto.trace package. It then calls the gt function from the operators module, passing the current Node instance (self) and another Node instance created from the other parameter using the node function. - -The node function is responsible for converting the other parameter into a Node object if it is not already one. This ensures that the comparison is always between two Node objects. The gt function from the operators module performs the actual comparison and returns the result. - -**Note**: -- The other parameter can be any object that can be converted into a Node using the node function. -- The comparison relies on the gt function from the operators module, which should be defined to handle Node comparisons appropriately. - -**Output Example**: A possible return value of the __gt__ method could be a boolean value, such as True or False, indicating whether the current Node instance is greater than the other object. -*** -### FunctionDef __ge__(self, other) -**__ge__**: The function of __ge__ is to compare the current Node object with another object to determine if the current Node is greater than or equal to the other object. - -**parameters**: The parameters of this function. -· self: The current instance of the Node object. -· other: The object to compare with the current Node. - -**Code Description**: The __ge__ method is a special method in Python used to define the behavior of the greater than or equal to (>=) operator for instances of a class. In this implementation, the method imports the `opto.trace.operators` module as `ops` and uses the `ge` function from this module to perform the comparison. - -The method first converts the `other` object into a Node object using the `node` function. This ensures that the comparison is always between two Node objects, regardless of the initial type of `other`. The `node` function is designed to create a Node object from a given message, handling various scenarios such as whether the message is already a Node, whether it should be trainable, and whether it has any constraints. - -Once the `other` object is converted into a Node, the `ge` function from the `ops` module is called with `self` and the newly created Node as arguments. The `ge` function is responsible for performing the actual comparison and returning the result. - -**Note**: -- The `__ge__` method ensures that comparisons are always made between Node objects by converting the `other` object using the `node` function. -- The `node` function handles various scenarios to create a Node object, making the comparison process robust and flexible. - -**Output Example**: A possible return value of the `__ge__` method could be a boolean value, such as `True` or `False`, indicating whether the current Node is greater than or equal to the `other` object. -*** -### FunctionDef __eq__(self, other) -**__eq__**: The function of __eq__ is to compare the current Node object with another object to determine if they are equal. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class. -· other: The object to compare with the current Node instance. - -**Code Description**: The __eq__ method is designed to enable comparison between a Node object and another object to check for equality. The method first checks if the 'other' object is an instance of the Node class. If it is, the method extracts the 'data' attribute from the 'other' Node object. Then, it compares the '_data' attribute of the current Node instance with the 'other' object (or its 'data' attribute if 'other' is a Node). The method returns True if the '_data' attributes are equal, and False otherwise. - -**Note**: -- This method overrides the default equality comparison behavior in Python. -- It ensures that two Node objects are considered equal if their '_data' attributes are equal. -- If 'other' is not a Node instance, the method directly compares 'self._data' with 'other'. - -**Output Example**: -- If `self._data` is 5 and `other` is a Node instance with `data` attribute 5, the method returns True. -- If `self._data` is 5 and `other` is 10, the method returns False. -*** -### FunctionDef __hash__(self) -**__hash__**: The function of __hash__ is to return the hash value of the Node object. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class. - -**Code Description**: The __hash__ method in the Node class is an override of the built-in __hash__ method. It calls the __hash__ method of its superclass using the super() function. This ensures that the hash value of the Node object is consistent with the hash value defined in its superclass. By doing so, it maintains the integrity and uniqueness of the hash value for instances of the Node class, which is crucial for operations that rely on hashing, such as using Node instances as keys in dictionaries or storing them in sets. - -**Note**: -- The __hash__ method should be consistent with the __eq__ method. If two objects are considered equal (using the __eq__ method), they must return the same hash value. -- Overriding the __hash__ method is essential when you need custom behavior for hashing, but in this case, it simply defers to the superclass implementation. - -**Output Example**: The return value of the __hash__ method will be an integer representing the hash value of the Node object, as determined by the superclass's __hash__ method. For example, if the superclass's __hash__ method returns 123456 for a particular Node instance, then calling hash(node_instance) will also return 123456. -*** -### FunctionDef __bool__(self) -**__bool__**: The function of __bool__ is to provide a boolean representation of the Node object. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class. - -**Code Description**: The __bool__ method is a special method in Python that is used to define the boolean value of an object. In this implementation, the method returns the boolean value of the instance variable `_data`. The expression `bool(self._data)` converts `_data` to its boolean equivalent. If `_data` is a non-empty value (such as a non-empty list, string, or a non-zero number), the method will return `True`. If `_data` is an empty value (such as an empty list, string, or zero), the method will return `False`. This allows the Node object to be used in boolean contexts, such as in conditional statements. - -**Note**: -- Ensure that the `_data` attribute is properly initialized in the Node class, as its value directly affects the boolean representation of the Node object. -- This method does not trace the conversion process, meaning it directly returns the boolean value without additional logging or processing. - -**Output Example**: -- If `_data` is a non-empty list, e.g., `[1, 2, 3]`, the return value will be `True`. -- If `_data` is an empty list, e.g., `[]`, the return value will be `False`. -*** -### FunctionDef format(self) -**format**: The function of format is to format the data contained within the Node object if the data is a string. - -**parameters**: The parameters of this Function. -· *args: Variable length argument list. -· **kwargs: Arbitrary keyword arguments. - -**Code Description**: The `format` function first checks if the `_data` attribute of the Node object is of type `str`. If `_data` is not a string, it raises an `AttributeError` indicating that the object does not have a `format` attribute. This ensures that only string data can be formatted using this function. - -Next, the function imports the `opto.trace.operators` module as `ops`. It then calls the `format` function from the `ops` module, passing the current Node object (`self`) along with any additional arguments (`*args`) and keyword arguments (`**kwargs`). This delegation allows the `format` function in the `ops` module to handle the actual formatting logic. - -**Note**: -- Ensure that the `_data` attribute of the Node object is a string before calling the `format` function to avoid an `AttributeError`. -- The `opto.trace.operators` module must be available and contain a `format` function that can handle the passed arguments and keyword arguments. - -**Output Example**: -If the `_data` attribute of the Node object is a string, the `format` function will return the formatted string as processed by the `opto.trace.operators.format` function. For example, if `_data` is `"Hello, {}"` and the arguments passed are `"World"`, the return value might be `"Hello, World"`. -*** -### FunctionDef capitalize(self) -**capitalize**: The function of capitalize is to convert the first character of the string stored in the `_data` attribute of the Node object to uppercase. - -**parameters**: This function does not take any parameters. - -**Code Description**: The `capitalize` function first checks if the `_data` attribute of the Node object is of type `str`. If `_data` is not a string, it raises an `AttributeError` indicating that the object does not have a `capitalize` attribute. This ensures that the function is only applied to string data. If `_data` is a string, the function imports the `capitalize` function from the `opto.trace.operators` module and returns the result of calling this `capitalize` function with the current Node object (`self`) as its argument. This modular approach allows for the actual capitalization logic to be handled by the `opto.trace.operators` module, promoting code reusability and separation of concerns. - -**Note**: -- Ensure that the `_data` attribute of the Node object is a string before calling the `capitalize` function to avoid raising an `AttributeError`. -- The function relies on the `opto.trace.operators` module, so make sure this module is correctly implemented and accessible. - -**Output Example**: If the `_data` attribute of the Node object is `"hello world"`, the `capitalize` function will return `"Hello world"`. -*** -### FunctionDef lower(self) -**lower**: The function of lower is to convert the string data contained within the object to lowercase. - -**parameters**: This function does not take any parameters. - -**Code Description**: The lower function is designed to operate on an instance's internal data, specifically converting it to lowercase if it is a string. The function first checks if the type of the instance's _data attribute is a string. If _data is not a string, it raises an AttributeError, indicating that the object does not have a 'lower' attribute. This ensures that the function only attempts to convert string data to lowercase, preventing type errors. If the _data attribute is a string, the function imports the lower function from the opto.trace.operators module and applies it to the instance, returning the result. - -**Note**: -- This function will raise an AttributeError if the _data attribute is not of type str. -- Ensure that the opto.trace.operators module is available and contains a lower function that can handle the conversion. - -**Output Example**: -If the _data attribute of the instance is "Hello World", the function will return "hello world". -*** -### FunctionDef upper(self) -**upper**: The function of upper is to convert the internal data of the Node object to uppercase if it is a string. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The upper function first checks if the internal data attribute (_data) of the Node object is of type string. If _data is not a string, it raises an AttributeError indicating that the object does not have an 'upper' attribute. If _data is a string, the function imports the upper function from the opto.trace.operators module and returns the result of calling this imported upper function with the current Node object as its argument. - -**Note**: -- This function will only work if the _data attribute of the Node object is a string. If _data is of any other type, an AttributeError will be raised. -- Ensure that the opto.trace.operators module is correctly implemented and accessible, as this function relies on it. - -**Output Example**: -If the _data attribute of the Node object is "hello", calling the upper function will return "HELLO". -*** -### FunctionDef swapcase(self) -**swapcase**: The function of swapcase is to convert all uppercase characters in the string to lowercase and vice versa. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The swapcase function is a method designed to operate on an instance's _data attribute. It first checks if the _data attribute is of type str. If _data is not a string, the function raises an AttributeError, indicating that the object does not have a swapcase attribute. This ensures that the function only processes string data. If the _data attribute is a string, the function imports the swapcase function from the opto.trace.operators module and applies it to the instance, returning the result. This modular approach allows for the swapcase operation to be defined and maintained separately in the operators module. - -**Note**: -- The _data attribute must be a string; otherwise, an AttributeError will be raised. -- Ensure that the opto.trace.operators module is correctly implemented and accessible. - -**Output Example**: -If the _data attribute of the instance is "Hello World", the swapcase function will return "hELLO wORLD". -*** -### FunctionDef title(self) -**title**: The function of title is to retrieve the title attribute of the Node object if it exists. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the Node class. - -**Code Description**: The title function checks if the _data attribute of the Node instance is a string. If _data is not a string, it raises an AttributeError indicating that the object does not have a title attribute. If _data is a string, it imports the title function from the opto.trace.operators module and returns the result of calling this imported title function with the current Node instance as its argument. - -**Note**: -- Ensure that the _data attribute of the Node instance is a string before calling the title function to avoid an AttributeError. -- The function relies on the title function from the opto.trace.operators module, so ensure that this module is correctly imported and available. - -**Output Example**: -If the _data attribute of the Node instance is a string, the function will return the result of the title function from the opto.trace.operators module. For example, if the title function in the operators module processes the string and returns a formatted title, the output will be that formatted title. -*** -### FunctionDef split(self, sep, maxsplit) -**split**: The function of split is to divide a string into a list of substrings based on a specified separator. - -**parameters**: The parameters of this Function. -· sep: The delimiter according to which the string is split. If not specified or None, any whitespace string is a separator. -· maxsplit: The maximum number of splits to do. -1 (the default value) means no limit on the number of splits. - -**Code Description**: The split function is designed to operate on an object that contains a string. It first checks if the object's _data attribute is of type str. If _data is not a string, it raises an AttributeError indicating that the split operation is not applicable to the object's data type. If _data is a string, the function imports the split function from the opto.trace.operators module and delegates the actual splitting operation to this imported function, passing along the separator and maxsplit parameters. - -**Note**: -- This function will raise an AttributeError if the _data attribute of the object is not a string. -- Ensure that the opto.trace.operators module is available and contains a split function that can handle the parameters passed to it. - -**Output Example**: -If the _data attribute of the object is "hello world" and the split function is called with the default parameters, the return value would be: -```python -['hello', 'world'] -``` -*** -### FunctionDef strip(self, chars) -**strip**: The function of strip is to remove leading and trailing characters from a string stored in the object's `_data` attribute. - -**parameters**: The parameters of this Function. -· chars: A string specifying the set of characters to be removed. If not provided, whitespace characters are removed by default. - -**Code Description**: The `strip` function first checks if the `_data` attribute of the object is of type `str`. If `_data` is not a string, it raises an `AttributeError` indicating that the object does not have a `strip` attribute. This ensures that the function is only applied to string data. The function then imports the `strip` function from the `opto.trace.operators` module and calls this imported `strip` function, passing the current object and the `chars` parameter to it. This design allows for the actual stripping operation to be handled by the `strip` function in the `opto.trace.operators` module, potentially allowing for more complex or customized stripping behavior. - -**Note**: -- Ensure that the `_data` attribute is a string before calling the `strip` function to avoid the `AttributeError`. -- The `chars` parameter is optional. If not provided, the function will default to removing whitespace characters. - -**Output Example**: -If `_data` is `" example "` and `chars` is not provided, the return value might be `"example"`. If `_data` is `"--example--"` and `chars` is `"-"`, the return value might be `"example"`. -*** -### FunctionDef replace(self, old, new, count) -**replace**: The function of replace is to substitute occurrences of a specified substring within the Node's data with a new substring. - -**parameters**: The parameters of this Function. -· self: The instance of the Node class. -· old: The substring that needs to be replaced. -· new: The substring that will replace the old substring. -· count: (optional) The maximum number of occurrences to replace. Default is -1, which means replace all occurrences. - -**Code Description**: The replace function is designed to perform a substring replacement operation on the data contained within a Node object. The function first checks if the data type of the Node's internal data (_data) is a string. If it is not a string, it raises an AttributeError, indicating that the replace operation is not applicable to the data type. - -The function then imports the replace function from the opto.trace.operators module. It proceeds to call this imported replace function, passing the current Node instance (self), and the old and new substrings wrapped in Node objects using the node function. The count parameter is also passed along to control the number of replacements. - -The node function is used to ensure that the old and new substrings are appropriately converted into Node objects if they are not already. This ensures consistency and proper handling within the replace operation. - -**Note**: -- The replace function only works if the Node's internal data is a string. Attempting to use it with non-string data will result in an AttributeError. -- The count parameter allows for partial replacements, where only a specified number of occurrences are replaced. If count is set to -1, all occurrences will be replaced. - -**Output Example**: A possible return value of the replace function could be a new Node object with the specified substring replacements applied to its internal string data. For instance, if the original Node's data is "hello world" and the replace function is called with old="world", new="there", the resulting Node's data would be "hello there". -*** -### FunctionDef items(self) -**items**: The function of items is to retrieve and return the items associated with the current instance of the Node class. - -**parameters**: The parameters of this Function. -· This function does not take any parameters other than the implicit 'self' which refers to the instance of the Node class. - -**Code Description**: The items function is designed to import the items function from the opto.trace.containers module and then call this imported function, passing the current instance (self) as an argument. This allows the function to retrieve the items related to the current Node instance by leveraging the functionality provided in the opto.trace.containers module. - -**Note**: -- Ensure that the opto.trace.containers module is correctly installed and accessible in your environment, as the items function relies on it. -- This function assumes that the imported items function from the opto.trace.containers module is designed to handle the Node instance appropriately. - -**Output Example**: -The return value of this function will depend on the implementation of the items function in the opto.trace.containers module. Typically, it might return a list, dictionary, or another collection of items associated with the Node instance. For example: -```python -[ - {'id': 1, 'name': 'Item1'}, - {'id': 2, 'name': 'Item2'} -] -``` -*** -### FunctionDef pop(self, __index) -**pop**: The function of pop is to remove and return an element from a Node object at a specified index. - -**parameters**: The parameters of this function. -· __index: An optional integer parameter that specifies the index of the element to be removed. The default value is -1, which means the last element will be removed. - -**Code Description**: The pop function is designed to remove and return an element from a Node object at a specified index. It imports the pop function from the opto.trace.operators module and utilizes the node function to handle the index parameter. The node function ensures that the index is properly converted into a Node object if it is not already one. This allows for consistent handling of the index parameter within the pop function. - -The pop function works as follows: -1. It imports the necessary operators from the opto.trace.operators module. -2. It calls the ops.pop function, passing the current Node object (self) and the index parameter converted to a Node object using the node function. - -The relationship with its callees is as follows: -- The node function is used to ensure that the index parameter is properly converted into a Node object. -- The ops.pop function from the opto.trace.operators module is used to perform the actual removal and return of the element from the Node object. - -**Note**: -- The default value of the __index parameter is -1, which means the last element will be removed if no index is specified. -- The node function is used to handle the index parameter, ensuring it is properly converted into a Node object. - -**Output Example**: A possible return value of the pop function could be the element that was removed from the Node object at the specified index. For example, if the Node object contained the elements [1, 2, 3] and the index parameter was 1, the return value would be 2, and the Node object would be updated to [1, 3]. -*** -### FunctionDef append(self) -**append**: The function of append is to add elements to a collection or list within the Node object. - -**parameters**: The parameters of this function. -· self: The instance of the Node class on which the method is called. -· *args: Variable-length positional arguments to be appended. -· **kwargs: Variable-length keyword arguments to be appended. - -**Code Description**: The `append` method is a member of the `Node` class in the `opto.trace.nodes.py` module. This method is designed to add elements to a collection or list within the Node object. It achieves this by internally calling the `call` method with the string "append" as the function name, along with any positional (`*args`) and keyword arguments (`**kwargs`) provided. - -The `call` method, which is invoked by `append`, dynamically calls the specified function (in this case, "append") on the `Node` object. It first converts all positional and keyword arguments to `Node` objects using the `node` function, ensuring that the arguments are compatible with the Node's internal structure. After conversion, it retrieves the "append" function from the `Node` object using `getattr` and invokes it with the converted arguments. - -This design allows the `append` method to flexibly handle various types of input while ensuring that all elements being appended are properly formatted as `Node` objects. - -**Note**: -- The `append` method relies on the `call` method to dynamically invoke the "append" function on the `Node` object. -- All arguments passed to `append` are converted to `Node` objects before being appended. -- The `self` parameter must be a valid instance of the `Node` class. - -**Output Example**: A possible return value of the `append` method could be the result of the "append" function invoked on the `Node` object with the provided arguments. For instance, if the "append" function adds elements to a list, the return value might be the updated list. -*** -## ClassDef ParameterNode -**ParameterNode**: The function of ParameterNode is to represent a trainable node in a computational graph. - -**attributes**: -- value: The initial value of the node. -- name: The name of the node. -- trainable: A boolean indicating whether the node is trainable or not. -- description: A string describing the node. -- constraint: A constraint on the node. -- info: Additional information about the node. - -**Code Description**: The ParameterNode class is a subclass of the Node class and represents a trainable node in a computational graph. It is used to store and manipulate data in the graph. The class has an initializer method that takes in various parameters such as value, name, trainable, description, constraint, and info. These parameters are used to initialize the attributes of the ParameterNode object. - -The initializer method also calls the initializer method of the superclass (Node) to set the value and name attributes. It then sets the trainable, description, constraint, and info attributes based on the provided parameters. Additionally, it adds the ParameterNode object to the 'parameter' dependency set. - -The ParameterNode class also defines a __str__ method that returns a string representation of the node. This method allows users to easily look up the node in the feedback dictionary. - -**Note**: -- The ParameterNode class inherits from the Node class, which is a data node in a directed graph. -- The value attribute represents the initial value of the node. -- The name attribute represents the name of the node. -- The trainable attribute indicates whether the node is trainable or not. -- The description attribute provides information about the node. -- The constraint attribute represents a constraint on the node. -- The info attribute stores additional information about the node. - -**Output Example**: -A possible return value of the __str__ method could be "ParameterNode: (name, dtype=, data=value)". -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize an instance of the ParameterNode class with specified attributes. - -**parameters**: The parameters of this Function. -· value: The initial value assigned to the ParameterNode. -· name: An optional name for the ParameterNode. Default is None. -· trainable: A boolean indicating whether the parameter is trainable. Default is True. -· description: A string describing the ParameterNode. Default is "[ParameterNode] This is a ParameterNode in a computational graph." -· constraint: An optional constraint applied to the parameter. Default is None. -· info: Additional optional information about the parameter. Default is None. - -**Code Description**: The __init__ function initializes a ParameterNode object by calling the constructor of its superclass with the provided parameters. It sets the initial value, name, trainable status, description, constraint, and additional information for the ParameterNode. After initializing the superclass, it adds the current instance to the '_dependencies' dictionary under the 'parameter' key. This ensures that the ParameterNode is properly registered within the computational graph's dependency management system. - -**Note**: Points to note about the use of the code -- Ensure that the 'value' parameter is provided when creating an instance of ParameterNode. -- The 'name', 'constraint', and 'info' parameters are optional and can be omitted if not needed. -- The 'trainable' parameter defaults to True, indicating that the parameter will be included in training processes unless explicitly set to False. -- The 'description' parameter provides a default description but can be customized as needed. -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to provide a string representation of the ParameterNode object. - -**parameters**: The parameters of this Function. -· self: The instance of the ParameterNode class. - -**Code Description**: The `__str__` method is designed to return a human-readable string that represents the current state of a `ParameterNode` object. This method is particularly useful for debugging and logging purposes, as it provides a concise summary of the node's key attributes. - -When called, the `__str__` method constructs a string that includes: -- The name of the node, accessed via `self.name`. This name is managed by the `name` method of the `AbstractNode` class, which returns the value of the private attribute `_name`. -- The data type of the node's data, obtained using `type(self._data)`. -- The actual data stored in the node, accessed via `self._data`. - -The string is formatted as follows: -``` -ParameterNode: ({self.name}, dtype={type(self._data)}, data={self._data}) -``` -This format ensures that the string includes the node's name, the type of its data, and the data itself, all in a clear and structured manner. - -**Note**: -- The `__str__` method should be used when a string representation of the `ParameterNode` is needed, such as in logging or debugging scenarios. -- Ensure that the node's data (`self._data`) is in a state that can be meaningfully represented as a string. - -**Output Example**: -If a `ParameterNode` object has a name "node:0", data type ``, and data `42`, the `__str__` method will return: -``` -ParameterNode: (node:0, dtype=, data=42) -``` -*** -## ClassDef MessageNode -**MessageNode**: The MessageNode class represents the output of an operator in a computational graph. - -**attributes**: -- value: The value of the node. -- inputs: The input nodes of the MessageNode. It can be a list or a dictionary. -- description: A string that describes the operator associated with the MessageNode. -- constraint: A constraint on the node. -- name: The name of the node. -- info: Additional information about the node. - -**Code Description**: -The MessageNode class is a subclass of the Node class and inherits its attributes and methods. It overrides the __init__ method to include the inputs, description, constraint, name, and info parameters. The inputs parameter can be a list or a dictionary, and it represents the input nodes of the MessageNode. The description parameter is a string that describes the operator associated with the MessageNode. The constraint parameter specifies a constraint on the node. The name parameter is the name of the node. The info parameter is additional information about the node. - -The __init__ method initializes the MessageNode by calling the __init__ method of the Node class and passing the value, name, description, constraint, and info parameters. It checks if the inputs parameter is a list or a dictionary and creates a dictionary with the names of the nodes as keys if it is a list. It then assigns the inputs to the _inputs attribute of the MessageNode. If the GRAPH.TRACE flag is False, it checks if the MessageNode has any inputs and raises an assertion error if it does. It adds the parents and dependencies if the GRAPH.TRACE flag is True. - -The inputs property returns a copy of the _inputs attribute. - -The __str__ method returns a string representation of the MessageNode, including its name, data type, and data. - -The _add_feedback method is called to add feedback from a child node. It adds the feedback to the _feedback attribute of the MessageNode. - -The external_dependencies property returns a set of external dependencies based on the info attribute of the MessageNode. - -The _add_dependencies method is called to add dependencies from a parent node. It adds the parameter and expandable dependencies to the _dependencies attribute of the MessageNode. - -**Note**: -- The MessageNode class is used to represent the output of an operator in a computational graph. -- The inputs parameter can be a list or a dictionary, and it represents the input nodes of the MessageNode. -- The description parameter is a string that describes the operator associated with the MessageNode. -- The constraint parameter specifies a constraint on the node. -- The name parameter is the name of the node. -- The info parameter is additional information about the node. - -**Output Example**: -A possible appearance of the MessageNode object when converted to a string could be: -"MessageNode: (node_name, dtype=, data=10)" -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize a MessageNode object with the given parameters. - -**parameters**: -- self: The instance of the class. -- value: The value of the MessageNode object. -- inputs: The inputs to the MessageNode object, which can be either a list or a dictionary of Node objects. -- description: The description of the MessageNode object. -- constraint: An optional constraint on the MessageNode object. -- name: An optional name for the MessageNode object. -- info: Additional information about the MessageNode object. - -**Code Description**: -The `__init__` function is the constructor of the MessageNode class. It initializes a MessageNode object with the provided parameters. The function first calls the constructor of the parent class, AbstractNode, passing the value, name, description, constraint, and info parameters. - -Next, the function checks if the inputs parameter is either a list or a dictionary. If it is not, an assertion error is raised with the message "Inputs to MessageNode must be a list or a dict." This ensures that the inputs are of the correct type. - -If the inputs parameter is a list, the function creates a dictionary with the names of the nodes as keys and the nodes themselves as values. This is done to ensure that the inputs can be accessed by their names. - -The function then assigns the inputs to the _inputs attribute of the MessageNode object. - -If the GRAPH.TRACE flag is not set, indicating that tracing is not enabled, the function asserts that the _inputs attribute is empty. This is because when not tracing, a MessageNode should have no inputs. - -Next, the function iterates over the items in the _inputs dictionary. For each item, it checks if the value is an instance of the Node class. If it is not, an assertion error is raised with the message "Input {k} is not a Node." This ensures that all inputs are valid Node objects. - -For each valid input, the function calls the _add_parent method of the MessageNode object to add the input as a parent. This method adds the parent node to the hierarchical structure of the graph. - -The function also calls the _add_dependencies method of the MessageNode object to add the dependencies on parameters and expandable nodes. This method updates the _dependencies attribute of the MessageNode object. - -Finally, if the external_dependencies attribute of the MessageNode object is not empty, indicating that there are external dependencies, the function adds the MessageNode object to the 'expandable' set of the _dependencies attribute. - -**Note**: -- The inputs parameter should be either a list or a dictionary of Node objects. -- When not tracing, a MessageNode should have no inputs. -- The inputs should be valid Node objects. -- The _add_parent method adds the parent node to the hierarchical structure of the graph. -- The _add_dependencies method adds the dependencies on parameters and expandable nodes to the MessageNode object. -- The external_dependencies attribute indicates the external dependencies of the MessageNode object. -*** -### FunctionDef inputs(self) -**inputs**: The function of inputs is to return a copy of the `_inputs` attribute of the object. - -**parameters**: -- self: The current object. - -**Code Description**: -The `inputs` function is a method of the `MessageNode` class. It returns a copy of the `_inputs` attribute of the object. The `_inputs` attribute is a dictionary that stores the input nodes of the `MessageNode` object. - -The purpose of this function is to provide access to the input nodes of the `MessageNode` object. By returning a copy of the `_inputs` attribute, it ensures that the original dictionary is not modified when accessing the input nodes. - -This function can be useful when you need to retrieve the input nodes of a `MessageNode` object for further processing or analysis. - -**Note**: -- The returned copy of the `_inputs` attribute is a shallow copy, which means that the keys and values of the dictionary are copied, but the objects themselves are not. If the values of the dictionary are mutable objects, modifying them will affect the original objects. -- The `_inputs` attribute is a private attribute and should not be modified directly. Use the `inputs` function to access the input nodes instead. - -**Output Example**: -``` -{ - 'input1': , - 'input2': , - ... -} -``` -*** -### FunctionDef __str__(self) -**__str__**: The function of __str__ is to provide a string representation of the MessageNode object. - -**parameters**: The parameters of this Function. -· self: The instance of the MessageNode class. - -**Code Description**: The __str__ method in the MessageNode class returns a formatted string that includes the name of the node, the data type of the node's data, and the data itself. This method is useful for debugging and logging purposes, as it provides a clear and concise representation of the node's state. - -The method calls the `name` method from the AbstractNode class to retrieve the name of the node. The `name` method returns the value of the private attribute `_name`, which is set when the node is registered in the graph. The `type(self._data)` function is used to get the data type of the node's data, and `self._data` is used to access the actual data stored in the node. - -The returned string follows the format: "MessageNode: (name, dtype=data_type, data=data)", where `name` is the node's name, `data_type` is the type of the data, and `data` is the actual data. - -**Note**: -- The __str__ method should be used when a string representation of the MessageNode object is needed, such as in logging or debugging scenarios. -- Ensure that the node has been properly initialized and registered before calling this method to avoid any unexpected behavior. - -**Output Example**: -If the name of the node is "node:0", the data type is ``, and the data is `42`, the __str__ method will return: -``` -MessageNode: (node:0, dtype=, data=42) -``` -*** -### FunctionDef _add_feedback(self, child, feedback) -**_add_feedback**: The function of _add_feedback is to add feedback from a child node. - -**parameters**: The parameters of this Function. -· child: The child node from which the feedback is received. -· feedback: The feedback data provided by the child node. - -**Code Description**: The _add_feedback function is designed to handle feedback from child nodes within a MessageNode. It first calls the parent class's _add_feedback method to ensure any inherited behavior is executed. After that, it asserts that the length of the feedback list for the given child node is exactly one. This assertion ensures that each child node provides only one piece of feedback, maintaining the integrity and expected behavior of the MessageNode. - -**Note**: -- This function relies on the parent class's _add_feedback method, so it is crucial that the parent class is correctly implemented. -- The assertion will raise an AssertionError if a child node provides more than one piece of feedback, which helps in debugging and maintaining the correct structure of feedback within the MessageNode. -*** -### FunctionDef external_dependencies(self) -**external_dependencies**: The function of external_dependencies is to determine the external dependencies of a MessageNode object. - -**parameters**: -- self: The MessageNode object itself. - -**Code Description**: -The `external_dependencies` function is a method within the `MessageNode` class that calculates and returns the external dependencies of the node. It checks if the `info` attribute of the `MessageNode` instance is a dictionary and if it contains an 'output' key that is an instance of the `Node` class. If these conditions are met, it compares the length of the parameter dependencies of the 'output' node with the parameter dependencies of the current `MessageNode`. If the 'output' node has more parameter dependencies, it returns the difference between the two sets of dependencies. This indicates that the `external_dependencies` function relies on the `parameter_dependencies` function of the `Node` class to determine the parameter dependencies of the nodes it interacts with. - -The purpose of the `external_dependencies` function is to identify any external dependencies that the `MessageNode` relies on, which are not already accounted for in its own parameter dependencies. By returning the set of external dependencies, users can gain insights into the dependencies of the `MessageNode` and ensure that all necessary dependencies are properly handled. - -It is important to note that the `external_dependencies` function assumes that the `info` attribute is a dictionary and that the 'output' key contains a valid `Node` object. If these assumptions are not met, the function will return an empty set. - -**Note**: -- The `external_dependencies` function relies on the `parameter_dependencies` function of the `Node` class to determine the parameter dependencies of the nodes it interacts with. -- The `info` attribute of the `MessageNode` instance must be a dictionary and contain an 'output' key that is an instance of the `Node` class for the function to work correctly. - -**Output Example**: A possible return value of the `external_dependencies` function could be a set of external dependencies, such as: -``` -{'dependency1', 'dependency2', 'dependency3'} -``` -*** -### FunctionDef _add_dependencies(self, parent) -**_add_dependencies**: The function of _add_dependencies is to add dependencies on parameters and expandable nodes to the current MessageNode object. - -**Parameters**: -- parent: The parent node to add as a dependency. - -**Code Description**: -The `_add_dependencies` function is used to add dependencies on parameters and expandable nodes to the current MessageNode object. It takes a `parent` parameter, which is the parent node to be added as a dependency. - -The function first checks if the `parent` is not the same as the current object itself. If it is, an assertion error is raised with the message "Cannot add self as a parent." - -Next, it checks if the `parent` is an instance of the `Node` class. If it is not, an assertion error is raised with a message indicating that the `parent` is not a Node. - -If both assertions pass, the function proceeds to add the dependencies. It updates the `_dependencies` dictionary of the current object by taking the union of the `parameter` and `expandable` dependencies of the `parent` node. This is done using the bitwise OR operator (`|`). - -Finally, the function returns without any explicit return value. - -**Note**: -- The `parent` parameter should be a valid Node object. -- The function assumes that the current object is a MessageNode. -- The function updates the `_dependencies` dictionary of the current object to include the dependencies from the `parent` node. -*** -## ClassDef ExceptionNode -**ExceptionNode**: The ExceptionNode class represents a node containing an exception message. - -**attributes**: -- value: The exception value. -- inputs: The input nodes of the ExceptionNode. It can be a list or a dictionary. -- description: A string that describes the ExceptionNode. -- constraint: A constraint on the node. -- name: The name of the node. -- info: Additional information about the node. - -**Code Description**: -The ExceptionNode class is a subclass of the MessageNode class and inherits its attributes and methods. It overrides the __init__ method to include the value, inputs, description, constraint, name, and info parameters. The value parameter represents the exception value. The inputs parameter can be a list or a dictionary, and it represents the input nodes of the ExceptionNode. The description parameter is a string that describes the ExceptionNode. The constraint parameter specifies a constraint on the node. The name parameter is the name of the node. The info parameter is additional information about the node. - -The __init__ method initializes the ExceptionNode by calling the __init__ method of the MessageNode class and passing the value, inputs, description, constraint, name, and info parameters. It checks if the value is an instance of trace.ExecutionError and formats the value accordingly. It then calls the __init__ method of the MessageNode class and passes the formatted value, inputs, description, constraint, name, and info parameters. - -**Note**: -- The ExceptionNode class represents a node containing an exception message. -- The value parameter represents the exception value. -- The inputs parameter can be a list or a dictionary, and it represents the input nodes of the ExceptionNode. -- The description parameter is a string that describes the ExceptionNode. -- The constraint parameter specifies a constraint on the node. -- The name parameter is the name of the node. -- The info parameter is additional information about the node. - -**Output Example**: -A possible appearance of the ExceptionNode object when converted to a string could be: -"ExceptionNode: (node_name, dtype=, data=10)" -### FunctionDef __init__(self, value) -**__init__**: The function of __init__ is to initialize an instance of the ExceptionNode class. - -**parameters**: -- value: The exception value to be stored in the ExceptionNode. -- inputs: The inputs to the ExceptionNode, which can be either a list of nodes or a dictionary of nodes. -- description: A string that describes the ExceptionNode. The default value is "[ExceptionNode] This is node containing the error of execution." -- constraint: An optional constraint on the ExceptionNode. -- name: An optional name for the ExceptionNode. -- info: Additional information about the ExceptionNode. - -**Code Description**: -The __init__ method of the ExceptionNode class initializes an instance of the ExceptionNode with the given parameters. It first assigns the value parameter to the variable e. Then, it uses regular expression to extract the error type from the string representation of the exception value. The re.search function searches for the pattern "" in the string and retrieves the matched group, which represents the error type. - -Next, it imports the trace module from the opto package. This import is necessary because the isinstance function is used later in the code. - -The code then checks if the value is an instance of the ExecutionError class from the trace module. If it is not, it formats the exception message by concatenating the error type and the string representation of the exception value. This ensures that the exception message is informative and includes the error type. - -Finally, the super().__init__ method is called to initialize the ExceptionNode instance with the value, inputs, description, constraint, name, and info parameters. The super() function is used to call the __init__ method of the base class (Node) and pass the parameters to it. - -**Note**: -- The ExceptionNode class is used to represent a node in a computational graph that contains an exception value. It is typically used to handle errors that occur during the execution of code within a tracing context. -- The value parameter should be an instance of the Exception class or a subclass of it. -- The inputs parameter should be a list of nodes or a dictionary of nodes that serve as inputs to the ExceptionNode. -- The description parameter is optional and can be used to provide additional information about the ExceptionNode. -- The constraint parameter is optional and can be used to specify a constraint on the ExceptionNode. -- The name parameter is optional and can be used to assign a name to the ExceptionNode. -- The info parameter is optional and can be used to provide additional information about the ExceptionNode. -- When creating an instance of the ExceptionNode class, make sure to provide the necessary inputs and ensure that the value parameter is an instance of the Exception class or a subclass of it. -*** diff --git a/generated_docs/opto/trace/operators.md b/generated_docs/opto/trace/operators.md deleted file mode 100644 index 0296eb16..00000000 --- a/generated_docs/opto/trace/operators.md +++ /dev/null @@ -1,893 +0,0 @@ -## FunctionDef clone(x) -**clone**: The function of clone is to create a deep copy of the input object `x`. - -**parameters**: The parameters of this Function. -· x: The object to be cloned. It can be of any type. - -**Code Description**: The `clone` function is designed to generate a deep copy of the provided object `x`. This is achieved using the `copy.deepcopy` method from Python's `copy` module. A deep copy means that all levels of the object are copied recursively, ensuring that the new object is entirely independent of the original. This is particularly useful when dealing with complex objects that contain nested structures, as it prevents changes in the cloned object from affecting the original object and vice versa. - -**Note**: -- Ensure that the `copy` module is imported before using the `clone` function. -- Be aware that deep copying can be resource-intensive for large or complex objects, as it involves duplicating every element within the object. - -**Output Example**: -If `x` is a list `[1, 2, [3, 4]]`, calling `clone(x)` will return a new list `[1, 2, [3, 4]]` that is a deep copy of `x`. Changes to the nested list in the cloned object will not affect the original list. -## FunctionDef identity(x) -**identity**: The function of identity is to return a duplicate of the input object. - -**parameters**: The parameters of this Function. -· x: Any - The input object that will be duplicated. - -**Code Description**: The identity function takes a single parameter, x, and returns a duplicate of this parameter by calling its clone method. The clone method is a part of the Node class, which creates and returns a duplicate of the current Node object. When identity is called with an object, it effectively behaves the same as calling the clone method on that object. This ensures that the original object remains unmodified, and a new instance with the same attributes and states is returned. - -The identity function is integral to operations that require object duplication within the project. It relies on the clone method from the Node class, which imports the clone function from the opto.trace.operators module and applies it to the current instance of the Node class. This standardized operation ensures consistency in how objects are duplicated across the project. - -**Note**: -- Ensure that the input object x has a clone method implemented; otherwise, the identity function will raise an AttributeError. -- The identity function does not modify the original object; it only creates and returns a duplicate. - -**Output Example**: If the input object x is an instance of the Node class with specific attributes and states, the return value of the identity function will be a new instance of the Node class that is a duplicate of the original instance. For example, if the original Node instance has attributes like name and value, the cloned instance will have the same name and value. -## FunctionDef pos(x) -**pos**: The function of pos is to return the unary positive of the input value x. - -**parameters**: The parameters of this Function. -· x: Any - The input value to which the unary positive operator will be applied. - -**Code Description**: The pos function takes a single parameter x and applies the unary positive operator to it. This operator is represented by the plus sign (+) in Python. The unary positive operator does not change the value of x; it simply returns x itself. This function is useful in contexts where the unary positive operator needs to be explicitly applied to a value. - -In the project, the pos function is called by the __pos__ method of the Node class located in opto\trace\nodes.py. When the unary positive operator is used on an instance of the Node class (e.g., +node_instance), the __pos__ method is invoked, which in turn calls the pos function from the opto.trace.operators module. This ensures that the unary positive operation is consistently applied to instances of the Node class. - -**Note**: -- The pos function does not alter the input value; it simply returns it. -- Ensure that the input value x is of a type that supports the unary positive operator. - -**Output Example**: -If the input value x is 5, the return value will be 5. -If the input value x is -3.2, the return value will be -3.2. -## FunctionDef neg(x) -**neg**: The function of neg is to return the negation of the input value. - -**parameters**: The parameters of this Function. -· x: The input value to be negated. It can be of any type that supports the unary negation operator. - -**Code Description**: The neg function takes a single parameter, x, and returns its negation. This is achieved using the unary negation operator (-). The function is designed to work with any type that supports this operator, such as integers, floats, and other numeric types. - -In the context of the project, the neg function is called by the __neg__ method of the Node class in the opto\trace\nodes.py module. When the unary negation operator is applied to an instance of the Node class (e.g., -node_instance), the __neg__ method is invoked. This method imports the neg function from the opto.trace.operators module and applies it to the instance, effectively negating the Node object. - -**Note**: Ensure that the input value x is of a type that supports the unary negation operator to avoid runtime errors. - -**Output Example**: If the input value x is 5, the function will return -5. If the input value x is -3.2, the function will return 3.2. -## FunctionDef abs(x) -**abs**: The function of abs is to return the absolute value of the input x. - -**parameters**: The parameters of this Function. -· x: Any - The input value for which the absolute value is to be calculated. - -**Code Description**: The abs function takes a single parameter x and returns its absolute value. The function is a straightforward wrapper around Python's built-in abs() function, which computes the absolute value of a given number. This function is designed to be used within the opto.trace.operators module. - -In the context of its usage within the project, the abs function is called by the __abs__ method of the Node class located in opto\trace\nodes.py. When the __abs__ method is invoked on an instance of the Node class, it imports the abs function from the opto.trace.operators module and applies it to the instance. This allows the Node class to leverage the abs function to compute the absolute value of its instances. - -**Note**: -- Ensure that the input x is a type that supports the absolute value operation, such as int, float, or any custom object that implements the __abs__ method. -- The function relies on Python's built-in abs() function, so its behavior and limitations are consistent with that. - -**Output Example**: -- If x is -5, the function will return 5. -- If x is 3.14, the function will return 3.14. -- If x is an instance of a custom class that implements the __abs__ method, the function will return the result of that method. -## FunctionDef invert(x) -**invert**: The function of invert is to perform a bitwise NOT operation on the input value x. - -**parameters**: The parameters of this Function. -· x: The input value on which the bitwise NOT operation will be performed. It can be of any type that supports the bitwise NOT operation. - -**Code Description**: The invert function takes a single parameter x and returns the result of applying the bitwise NOT operation to x. The bitwise NOT operation, denoted by the tilde (~) operator, inverts each bit of the input value. For example, if x is an integer, each bit in the binary representation of x will be flipped (0s become 1s and 1s become 0s). - -In the context of the project, the invert function is called by the __invert__ method of the Node class in the opto\trace\nodes.py module. When the __invert__ method is invoked on an instance of the Node class, it imports the invert function from the opto.trace.operators module and applies it to the instance. This allows the Node class to support the bitwise NOT operation using the ~ operator. - -**Note**: Ensure that the input value x is of a type that supports the bitwise NOT operation. Using types that do not support this operation will result in a TypeError. - -**Output Example**: -- If x is an integer with a value of 5, the return value will be -6. -- If x is an integer with a value of 0, the return value will be -1. -## FunctionDef round(x, n) -**round**: The function of round is to round a given value `x` to a specified number of decimal places `n`. - -**parameters**: The parameters of this Function. -· x: The value to be rounded. This can be of any type that supports rounding. -· n: The number of decimal places to round to. This can be of any type that can be interpreted as an integer. - -**Code Description**: The `round` function is designed to round a given value `x` to `n` decimal places. It takes two parameters: `x`, which is the value to be rounded, and `n`, which specifies the number of decimal places to round to. The function returns the result of the built-in `round` function applied to these parameters. - -In the context of its usage within the project, the `round` function is called by the `__round__` method of the `Node` class in the `opto\trace\nodes.py` file. The `__round__` method imports the `round` function from `opto.trace.operators` and applies it to the instance of the `Node` class (`self`). If a parameter `n` is provided, it is passed to the `round` function; otherwise, `None` is passed. - -**Note**: -- Ensure that the types of `x` and `n` are compatible with the built-in `round` function to avoid runtime errors. -- The `round` function in this context is a wrapper around Python's built-in `round` function, so it inherits its behavior and limitations. - -**Output Example**: -If `x` is 3.14159 and `n` is 2, the function will return 3.14. -If `x` is 3.14159 and `n` is 0, the function will return 3. -## FunctionDef floor(x) -**floor**: The function of floor is to compute the largest integer less than or equal to a given number x. - -**parameters**: The parameters of this Function. -· x: A numeric value of any type (int, float, etc.) that you want to apply the floor operation to. - -**Code Description**: The floor function takes a single parameter x and returns the largest integer less than or equal to x. Internally, it uses the `math.floor` method from Python's math module to perform this operation. This function is useful in scenarios where you need to round down a floating-point number to the nearest whole number. - -In the project, this function is called by the `__floor__` method of the `Node` class located in `opto\trace\nodes.py`. The `__floor__` method imports the `floor` function from `opto.trace.operators` and applies it to the instance of the `Node` class. This indicates that the `Node` class instances can be floored directly, leveraging the `floor` function to achieve this. - -**Note**: Ensure that the input parameter x is a numeric value; otherwise, the function will raise a TypeError. This function is dependent on the `math` module, so ensure that it is available in your environment. - -**Output Example**: -- If `x` is 3.7, `floor(x)` will return 3. -- If `x` is -2.3, `floor(x)` will return -3. -## FunctionDef ceil(x) -**ceil**: The function of ceil is to return the smallest integer greater than or equal to a given number. - -**parameters**: The parameters of this Function. -· x: A numeric value of any type (int, float, etc.) that you want to round up to the nearest integer. - -**Code Description**: The ceil function is designed to round up a given numeric value to the nearest integer. It imports the math module and utilizes the math.ceil() method to perform this operation. The function takes a single parameter, x, which can be any numeric type. When called, it returns the smallest integer that is greater than or equal to x. - -In the context of the project, the ceil function is called by the __ceil__ method of the Node class located in opto\trace\nodes.py. This indicates that the Node class leverages the ceil function to provide a ceiling operation on its instances. When __ceil__ is invoked on a Node object, it imports the ceil function from opto.trace.operators and applies it to the Node instance, effectively rounding up the value represented by the Node. - -**Note**: Ensure that the input parameter x is a numeric value; otherwise, the function will raise a TypeError. The function relies on the math module, so it must be available in the environment where the code is executed. - -**Output Example**: -- If x = 4.2, ceil(x) will return 5. -- If x = -3.7, ceil(x) will return -3. -- If x = 7, ceil(x) will return 7. -## FunctionDef trunc(x) -**trunc**: The function of trunc is to truncate the decimal part of a number, returning the integer part. - -**parameters**: The parameters of this Function. -· x: The number to be truncated. It can be of any type that is compatible with the math.trunc function, typically an integer or a float. - -**Code Description**: The trunc function is designed to truncate the decimal part of a given number, effectively returning its integer part. This is achieved by utilizing the math.trunc function from Python's math module. When the trunc function is called with a number x, it imports the math module and then applies math.trunc to x, returning the truncated integer value. - -In the context of the project, the trunc function is called by the __trunc__ method of the Node class located in opto\trace\nodes.py. The __trunc__ method imports the trunc function from opto.trace.operators and applies it to the instance of the Node class. This indicates that the Node class instances can be truncated using the trunc function, ensuring that any Node object can be converted to its integer representation if needed. - -**Note**: -- Ensure that the input x is a type that can be handled by the math.trunc function, such as an integer or a float. -- The function will raise a TypeError if x is not a number. - -**Output Example**: -If the input x is 3.14, the function will return 3. -If the input x is -2.99, the function will return -2. -## FunctionDef add(x, y) -**add**: The function of add is to perform an addition operation on two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type. -· y: The second operand, which can be of any type. - -**Code Description**: The add function takes two parameters, x and y, and returns their sum. The function is designed to handle operands of any type, leveraging Python's dynamic typing and operator overloading capabilities. This means that the function can add numbers, concatenate strings, or combine other compatible types as defined by the '+' operator in Python. - -In the project, the add function is utilized in the __add__ method of the Node class located in opto\trace\nodes.py. When the __add__ method is called on a Node object, it imports the add function from opto.trace.operators and uses it to add the Node's data to another operand. This demonstrates the function's flexibility in handling different types of data within the Node class. - -**Note**: Ensure that the types of x and y are compatible with the '+' operator to avoid runtime errors. For example, adding a string to an integer will raise a TypeError. - -**Output Example**: -- If x = 3 and y = 5, add(x, y) will return 8. -- If x = "Hello" and y = " World", add(x, y) will return "Hello World". -## FunctionDef subtract(x, y) -**subtract**: The function of subtract is to perform a subtraction operation between two operands, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the subtraction operation. -· y: The second operand, which can be of any type that supports the subtraction operation. - -**Code Description**: The subtract function takes two parameters, x and y, and returns the result of subtracting y from x. This function is designed to handle any data types that support the subtraction operator (-). In the context of the project, this function is utilized by the __sub__ method of the Node class in the opto\trace\nodes.py module. When the subtraction operator (-) is used between two Node objects, the __sub__ method is invoked, which in turn calls the subtract function from the opto.trace.operators module. This allows for a seamless and consistent subtraction operation between Node objects. - -**Note**: Ensure that the operands x and y are of compatible types that support the subtraction operation to avoid runtime errors. - -**Output Example**: -- If x = 10 and y = 5, the function will return 5. -- If x = [1, 2, 3] and y = [1, 1, 1], the function will return [0, 1, 2] (assuming the operands are lists and the subtraction operation is defined for lists in this context). -## FunctionDef multiply(x, y) -**multiply**: The function of multiply is to perform a multiplication operation between two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand in the multiplication operation. It can be of any type that supports the multiplication operator (*). -· y: The second operand in the multiplication operation. It can be of any type that supports the multiplication operator (*). - -**Code Description**: The multiply function takes two parameters, x and y, and returns the result of multiplying these two parameters using the multiplication operator (*). This function is designed to be generic and can handle any types of inputs that support the multiplication operation. - -In the context of the project, the multiply function is called by the __mul__ method of the Node class in the opto\trace\nodes.py module. When the __mul__ method is invoked, it imports the multiply function from the opto.trace.operators module and applies it to the current instance (self) and another operand (other). This allows for the multiplication of Node objects or Node-compatible objects using the * operator. - -**Note**: Ensure that the types of x and y are compatible with the multiplication operator to avoid runtime errors. If either x or y does not support multiplication, a TypeError will be raised. - -**Output Example**: -- If x = 3 and y = 4, multiply(x, y) will return 12. -- If x = [1, 2] and y = 3, multiply(x, y) will return [1, 2, 1, 2, 1, 2]. -## FunctionDef floor_divide(x, y) -**floor_divide**: The function of floor_divide is to perform floor division between two operands, x and y. - -**parameters**: The parameters of this Function. -· x: The dividend, which can be of any type that supports the floor division operation. -· y: The divisor, which can be of any type that supports the floor division operation. - -**Code Description**: The floor_divide function takes two parameters, x and y, and returns the result of the floor division operation (x // y). Floor division is an operation that divides two numbers and rounds down the result to the nearest integer. This function is designed to handle any types that support the floor division operator (//). - -In the context of the project, the floor_divide function is called by the __floordiv__ method of the Node class in the opto\trace\nodes.py module. When the __floordiv__ method is invoked on a Node object with another operand, it imports the floor_divide function from the opto.trace.operators module and applies it to the Node object and the other operand. This indicates that the floor_divide function is integral to the Node class's ability to handle floor division operations, ensuring that the operation is performed correctly and consistently within the project's framework. - -**Note**: Ensure that both x and y are of types that support the floor division operation to avoid runtime errors. The function does not perform type checking or validation, so improper types may lead to unexpected behavior or exceptions. - -**Output Example**: -If x = 7 and y = 3, the function call floor_divide(7, 3) will return 2, as 7 // 3 equals 2. -## FunctionDef divide(x, y) -**divide**: The function of divide is to perform division between two operands, x and y. - -**parameters**: The parameters of this Function. -· x: The dividend, which can be of any type that supports division. -· y: The divisor, which can be of any type that supports division. - -**Code Description**: The divide function takes two parameters, x and y, and returns the result of dividing x by y. This function is designed to handle any types that support the division operation. It is a straightforward implementation of the division operator, encapsulated within a function for modularity and reuse. - -In the context of the project, the divide function is called by the __truediv__ method of the Node class located in opto\trace\nodes.py. When the division operator (/) is used between two Node objects, the __truediv__ method is invoked. This method imports the divide function from opto.trace.operators and applies it to the current Node instance (self) and the other operand (other), which is converted to a Node if it is not already one. This ensures that the division operation is consistently handled within the framework of Node objects. - -**Note**: Ensure that the divisor y is not zero to avoid a ZeroDivisionError. Additionally, both x and y should be of compatible types that support the division operation. - -**Output Example**: -If x is 10 and y is 2, the function will return 5.0. -If x is 9 and y is 3, the function will return 3.0. -## FunctionDef mod(x, y) -**mod**: The function of mod is to perform the modulo operation between two values, x and y. - -**parameters**: The parameters of this Function. -· x: The dividend in the modulo operation. It can be of any type that supports the modulo operation. -· y: The divisor in the modulo operation. It can be of any type that supports the modulo operation. - -**Code Description**: The mod function takes two parameters, x and y, and returns the result of the modulo operation (x % y). This operation finds the remainder when x is divided by y. The function is designed to handle any types that support the modulo operation, making it versatile for various use cases. - -In the project, this function is utilized by the __mod__ method of the Node class in the opto\trace\nodes.py module. When the __mod__ method is called on a Node object with another value, it imports the mod function from the opto.trace.operators module and applies it to the Node object and the other value. This integration allows Node objects to use the modulo operation seamlessly with other values, enhancing their arithmetic capabilities. - -**Note**: Ensure that both x and y are of types that support the modulo operation to avoid runtime errors. - -**Output Example**: -- If x is 10 and y is 3, the return value will be 1. -- If x is 20 and y is 7, the return value will be 6. -## FunctionDef divmod(x, y) -**divmod**: The function of divmod is to perform the divmod operation on two inputs, x and y, and return the result. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the divmod operation. -· y: The second operand, which can be of any type that supports the divmod operation. - -**Code Description**: The divmod function takes two parameters, x and y, and applies the built-in Python divmod function to them. The divmod function returns a tuple containing the quotient and the remainder when dividing x by y. This function is a straightforward wrapper around Python's built-in divmod, providing a consistent interface for performing this operation within the project. - -In the context of its usage within the project, the divmod function is called by the __divmod__ method of the Node class in the opto\trace\nodes.py module. When the __divmod__ method is invoked on a Node object, it imports the divmod function from the opto.trace.operators module and applies it to the Node instance and another operand. This integration ensures that the divmod operation can be seamlessly used with Node objects, allowing for consistent and predictable behavior when performing division and modulus operations within the project's tracing framework. - -**Note**: Ensure that both x and y are of types that support the divmod operation to avoid runtime errors. The function relies on Python's built-in divmod, so the behavior and constraints of the built-in function apply here as well. - -**Output Example**: -If x is 10 and y is 3, the return value will be (3, 1), where 3 is the quotient and 1 is the remainder. -## FunctionDef power(x, y) -**power**: The function of power is to compute the result of raising x to the power of y. - -**parameters**: The parameters of this Function. -· x: The base value, which can be of any type that supports the power operation. -· y: The exponent value, which can be of any type that supports the power operation. - -**Code Description**: The power function takes two arguments, x and y, and returns the result of x raised to the power of y (x**y). This function is a simple implementation of the power operation and relies on Python's built-in exponentiation operator (**). - -In the context of the project, this function is utilized by the __pow__ method of the Node class in the opto\trace\nodes.py module. When the __pow__ method is called on a Node object with another value, it imports the power function from the opto.trace.operators module and applies it to the Node object and the other value. This allows for the use of the power operator (**) directly on Node objects, enabling more intuitive mathematical operations within the project's framework. - -**Note**: Ensure that the types of x and y are compatible with the power operation to avoid runtime errors. - -**Output Example**: -If x is 2 and y is 3, the function will return 8, as 2**3 equals 8. -## FunctionDef lshift(x, y) -**lshift**: The function of lshift is to perform a left bitwise shift operation on two given inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the left shift operation. -· y: The second operand, which can be of any type that supports the left shift operation. - -**Code Description**: The lshift function takes two parameters, x and y, and returns the result of the left bitwise shift operation (x << y). This operation shifts the bits of x to the left by the number of positions specified by y. The function is designed to work with any types that support the left shift operation, typically integers. - -In the context of the project, the lshift function is called by the __lshift__ method of the Node class in the opto\trace\nodes.py module. The __lshift__ method imports the lshift function from the opto.trace.operators module and applies it to the current instance (self) and another operand (other). This indicates that the Node class uses the lshift function to define its own left shift behavior, allowing instances of Node to be shifted left using the << operator. - -**Note**: Ensure that the operands x and y are of types that support the left shift operation to avoid runtime errors. - -**Output Example**: -If x is 4 (binary 100) and y is 2, the function will return 16 (binary 10000), as the bits of 4 are shifted left by 2 positions. -## FunctionDef rshift(x, y) -**rshift**: The function of rshift is to perform a bitwise right shift operation on two operands, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the right shift operation. -· y: The second operand, which can be of any type that supports the right shift operation. - -**Code Description**: The rshift function takes two parameters, x and y, and returns the result of the bitwise right shift operation (x >> y). This operation shifts the bits of x to the right by the number of positions specified by y. The function is designed to handle any type that supports the right shift operation, typically integers. - -In the context of its usage within the project, the rshift function is called by the __rshift__ method of the Node class in the opto\trace\nodes.py module. The __rshift__ method imports the rshift function from the opto.trace.operators module and applies it to the current instance (self) and another node (other). This indicates that the rshift function is used to facilitate bitwise right shift operations between nodes within the project. - -**Note**: Ensure that the operands x and y are of types that support the right shift operation to avoid runtime errors. - -**Output Example**: -If x is 8 (binary 1000) and y is 2, the function call rshift(8, 2) will return 2 (binary 10). -## FunctionDef and_(x, y) -**and_**: The function of and_ is to perform a bitwise AND operation between two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand, which can be of any type that supports the bitwise AND operation. -· y: The second operand, which can be of any type that supports the bitwise AND operation. - -**Code Description**: The and_ function takes two parameters, x and y, and returns the result of the bitwise AND operation between them. This operation is denoted by the '&' symbol in Python. The function is straightforward and relies on Python's built-in bitwise AND operator to compute the result. - -In the context of its usage within the project, the and_ function is called by the __and__ method of the Node class in the opto\trace\nodes.py module. When the __and__ method is invoked on a Node object with another operand, it imports the and_ function from the opto.trace.operators module and applies it to the Node instance and the other operand. This allows for a seamless bitwise AND operation between Node objects or between a Node object and another compatible operand. - -**Note**: Ensure that the operands x and y are of types that support the bitwise AND operation to avoid any runtime errors. - -**Output Example**: -If x = 6 (binary 110) and y = 3 (binary 011), the function call and_(6, 3) will return 2 (binary 010). -## FunctionDef or_(x, y) -**or_**: The function of or_ is to perform a bitwise OR operation between two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first operand for the bitwise OR operation. It can be of any type that supports the bitwise OR operation. -· y: The second operand for the bitwise OR operation. It can be of any type that supports the bitwise OR operation. - -**Code Description**: The or_ function takes two parameters, x and y, and returns the result of the bitwise OR operation between them. The bitwise OR operation is denoted by the "|" operator in Python. This function is designed to be a utility function that can be used wherever a bitwise OR operation is needed. - -In the context of its usage within the project, the or_ function is called by the __or__ method of the Node class in the opto\trace\nodes.py module. The __or__ method imports the or_ function from the opto.trace.operators module and applies it to the current Node instance (self) and another Node instance (other). This allows for the use of the "|" operator to combine two Node instances using the bitwise OR operation. - -**Note**: Ensure that the operands x and y are of types that support the bitwise OR operation to avoid TypeErrors. - -**Output Example**: If x is 5 (binary 0101) and y is 3 (binary 0011), the return value of or_(x, y) would be 7 (binary 0111). -## FunctionDef xor(x, y) -**xor**: The function of xor is to perform a bitwise XOR operation between two inputs, x and y. - -**parameters**: The parameters of this Function. -· x: Any - The first operand for the XOR operation. -· y: Any - The second operand for the XOR operation. - -**Code Description**: The xor function takes two parameters, x and y, and returns the result of the bitwise XOR operation between them. The bitwise XOR operation compares each bit of its operands and returns 1 if the bits are different, and 0 if they are the same. This function is useful in various scenarios, such as cryptography, error detection, and correction algorithms. - -In the context of the project, the xor function is called by the __xor__ method of the Node class in the opto\trace\nodes.py module. The __xor__ method imports the xor function from the opto.trace.operators module and applies it to the current Node instance and another Node instance or value. This allows for the use of the ^ operator to perform a bitwise XOR operation between Node objects, enhancing the functionality and usability of the Node class. - -**Note**: Ensure that the inputs x and y are of types that support the bitwise XOR operation, such as integers or objects that implement the __xor__ method. - -**Output Example**: Mock up a possible appearance of the code's return value. -If x = 5 (binary 0101) and y = 3 (binary 0011), the result of xor(x, y) would be 6 (binary 0110). -## FunctionDef lt(x, y) -**lt**: The function of lt is to compare two values and determine if the first value is less than the second value. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any type that supports comparison operations. -· y: The second value to be compared. It can be of any type that supports comparison operations. - -**Code Description**: The lt function takes two parameters, x and y, and returns the result of the comparison x < y. This function leverages Python's built-in less-than operator to perform the comparison. The function is designed to work with any data types that support the less-than comparison, such as integers, floats, and strings. The function returns a boolean value: True if x is less than y, and False otherwise. - -**Note**: -- Ensure that the types of x and y are compatible for comparison to avoid TypeError. -- This function does not handle cases where x and y are of different types that cannot be compared directly. - -**Output Example**: -- lt(3, 5) returns True because 3 is less than 5. -- lt(10, 2) returns False because 10 is not less than 2. -- lt('apple', 'banana') returns True because 'apple' is lexicographically less than 'banana'. -## FunctionDef le(x, y) -**le**: The function of le is to compare two values, x and y, and determine if x is less than or equal to y. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any type that supports comparison operations. -· y: The second value to be compared. It can be of any type that supports comparison operations. - -**Code Description**: The le function performs a comparison between two values, x and y, using the less than or equal to (<=) operator. It returns a boolean value: True if x is less than or equal to y, and False otherwise. This function is useful in scenarios where you need to enforce or check ordering constraints between two values. - -**Note**: -- Ensure that the types of x and y are compatible for comparison. If they are not, a TypeError will be raised. -- This function relies on the underlying implementation of the <= operator for the types of x and y. - -**Output Example**: -- le(3, 5) returns True because 3 is less than 5. -- le(5, 5) returns True because 5 is equal to 5. -- le(7, 5) returns False because 7 is greater than 5. -## FunctionDef eq(x, y) -**eq**: The function of eq is to compare two values, x and y, for equality. - -**parameters**: The parameters of this function. -· x: The first value to be compared. It can be of any data type. -· y: The second value to be compared. It can be of any data type. - -**Code Description**: The eq function takes two parameters, x and y, and returns a boolean value indicating whether the two parameters are equal. The comparison is performed using the equality operator (==), which checks if the values of x and y are the same. This function is useful for determining if two variables or objects hold the same value or state. - -**Note**: -- The function relies on the built-in equality operator (==), so the behavior of the comparison depends on how the equality operator is implemented for the data types of x and y. -- If x and y are of different types, the function will return False unless the types are comparable and considered equal by the equality operator. - -**Output Example**: -- eq(5, 5) returns True -- eq('hello', 'hello') returns True -- eq([1, 2, 3], [1, 2, 3]) returns True -- eq(5, '5') returns False -## FunctionDef ne(x, y) -**ne**: The function of ne is to compare two values, x and y, and determine if they are not equal. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any data type. -· y: The second value to be compared. It can be of any data type. - -**Code Description**: The ne function takes two parameters, x and y, and returns a boolean value indicating whether x is not equal to y. The function uses the != operator to perform the comparison. If x and y are not equal, the function returns True; otherwise, it returns False. This function is useful for scenarios where you need to check inequality between two values. - -**Note**: -- Ensure that the data types of x and y are compatible for comparison to avoid unexpected results. -- This function does not perform type conversion; it strictly compares the values as they are. - -**Output Example**: -- ne(5, 3) returns True because 5 is not equal to 3. -- ne('apple', 'orange') returns True because the strings 'apple' and 'orange' are not equal. -- ne(10, 10) returns False because both values are equal. -## FunctionDef ge(x, y) -**ge**: The function of ge is to compare two values and determine if the first value is greater than or equal to the second value. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any type that supports comparison operations. -· y: The second value to be compared. It can be of any type that supports comparison operations. - -**Code Description**: The ge function takes two parameters, x and y, and returns the result of the comparison x >= y. This means it checks if x is greater than or equal to y. The function leverages Python's built-in comparison operators to perform this task. The return value is a boolean: True if x is greater than or equal to y, and False otherwise. - -**Note**: -- Ensure that the types of x and y are compatible for comparison to avoid TypeErrors. -- This function is useful in scenarios where conditional logic is based on the comparison of two values. - -**Output Example**: -- ge(5, 3) returns True because 5 is greater than 3. -- ge(2, 2) returns True because 2 is equal to 2. -- ge(1, 4) returns False because 1 is not greater than or equal to 4. -## FunctionDef gt(x, y) -**gt**: The function of gt is to compare two values and determine if the first value is greater than the second value. - -**parameters**: The parameters of this Function. -· x: The first value to be compared. It can be of any type that supports the greater-than (>) comparison. -· y: The second value to be compared. It can be of any type that supports the greater-than (>) comparison. - -**Code Description**: The gt function takes two parameters, x and y, and returns the result of the comparison x > y. This means that the function evaluates whether the value of x is greater than the value of y. The function is designed to work with any data types that support the greater-than comparison operator. The return value is a boolean: True if x is greater than y, and False otherwise. - -**Note**: -- Ensure that the types of x and y are compatible for comparison using the greater-than operator. If the types are not compatible, a TypeError will be raised. -- This function does not perform any type checking or validation, so it is the responsibility of the user to provide appropriate arguments. - -**Output Example**: -- gt(5, 3) returns True because 5 is greater than 3. -- gt(2, 4) returns False because 2 is not greater than 4. -- gt('b', 'a') returns True because 'b' is greater than 'a' in lexicographical order. -## FunctionDef cond(condition, x, y) -**cond**: The function of cond is to select and return `x` if `condition` is True, otherwise it returns `y`. - -**parameters**: The parameters of this Function. -· condition: A boolean or any value that can be evaluated as a boolean. -· x: The value to be returned if `condition` is True. -· y: The value to be returned if `condition` is False. - -**Code Description**: The `cond` function is a simple utility that evaluates a given `condition` and returns one of two provided values based on the result of that evaluation. Specifically, if `condition` evaluates to True, the function returns `x`; otherwise, it returns `y`. - -The function begins by ensuring that all input data (`x`, `y`, and `condition`) are read and assigned to local variables. This step is somewhat redundant in this context but ensures that the inputs are processed. The core logic is implemented in a single return statement that uses a conditional expression (ternary operator) to decide which value to return based on the truthiness of `condition`. - -This function is called in the project by unit tests located in `tests\unit_tests\test_nodes.py`. These tests likely verify the correctness of the `cond` function by passing various conditions and corresponding values for `x` and `y`, ensuring that the function returns the expected result in each case. - -**Note**: -- Ensure that `condition` is a value that can be evaluated as a boolean. -- The function does not perform any type checking or validation on the inputs. - -**Output Example**: -- If `condition` is True, `x` is returned. -- If `condition` is False, `y` is returned. - -For instance: -- `cond(True, 'apple', 'orange')` returns `'apple'`. -- `cond(False, 'apple', 'orange')` returns `'orange'`. -## FunctionDef not_(x) -**not_**: The function of not_ is to return the logical negation of the input value x. - -**parameters**: The parameters of this Function. -· x: Any - The input value to be negated. - -**Code Description**: The not_ function takes a single parameter x of any type and returns the logical negation of x. In Python, the logical negation operator `not` is used to invert the truth value of the operand. If x is a truthy value (e.g., True, non-zero numbers, non-empty collections), the function will return False. Conversely, if x is a falsy value (e.g., False, 0, None, empty collections), the function will return True. This function is useful for scenarios where you need to invert the boolean value of a given input. - -**Note**: -- The input parameter x can be of any type, but the function will evaluate its truthiness according to Python's standard rules for boolean context. -- Ensure that the input value is appropriate for logical negation to avoid unexpected results. - -**Output Example**: -- not_(True) will return False. -- not_(0) will return True. -- not_([1, 2, 3]) will return False. -- not_('') will return True. -## FunctionDef is_(x, y) -**is_**: The function of is_ is to determine whether x is equal to y using identity comparison. - -**parameters**: The parameters of this Function. -· x: The first object to be compared. -· y: The second object to be compared. - -**Code Description**: The is_ function checks if the two provided arguments, x and y, are the same object in memory. This is done using the identity operator `is`, which returns True if both x and y refer to the same object, and False otherwise. This type of comparison is different from the equality operator `==`, which checks if the values of the objects are equal, not necessarily if they are the same object. - -**Note**: -- Use this function when you need to verify that two variables point to the exact same object, not just equivalent values. -- This function is particularly useful when dealing with singleton objects or when you need to ensure that two references are indeed pointing to the same memory location. - -**Output Example**: -- `is_(a, b)` returns `True` if `a` and `b` are the same object. -- `is_(a, b)` returns `False` if `a` and `b` are different objects, even if they have the same content. -## FunctionDef is_not(x, y) -**is_not**: The function of is_not is to determine whether two variables, `x` and `y`, are not the same object in memory. - -**parameters**: The parameters of this Function. -· x: The first variable to be compared. -· y: The second variable to be compared. - -**Code Description**: The `is_not` function checks if the two provided variables, `x` and `y`, do not refer to the same object in memory. This is achieved using the `is not` operator in Python, which returns `True` if `x` and `y` are not the same object, and `False` otherwise. This function is useful when you need to ensure that two variables are distinct objects, rather than just having the same value. - -**Note**: -- This function checks for object identity, not equality of values. Two different objects with the same value will still return `True`. -- This function is particularly useful in scenarios where object identity is crucial, such as when dealing with mutable objects or singleton patterns. - -**Output Example**: -- `is_not(5, 5)` would return `False` because both `5`s are the same immutable integer object. -- `is_not([], [])` would return `True` because each `[]` creates a new list object in memory. -- `is_not(a, b)` where `a` and `b` are references to the same object would return `False`. -## FunctionDef in_(x, y) -**in_**: The function of in_ is to determine whether an element x is present within a collection y. - -**parameters**: The parameters of this Function. -· x: The element to be checked for presence within the collection y. -· y: The collection in which the presence of element x is to be checked. - -**Code Description**: The in_ function takes two parameters, x and y, and returns a boolean value indicating whether x is present in y. This is achieved using Python's built-in membership operator `in`, which checks for the presence of an element within a collection such as a list, tuple, set, or dictionary. The function is straightforward and leverages Python's efficient membership testing capabilities. - -In the context of its usage within the project, the in_ function is called by the __contains__ method of the Node class in the opto\trace\nodes.py module. The __contains__ method uses the in_ function to determine if a given item is part of the Node instance. This is done by importing the in_ function from the opto.trace.operators module and applying it to the item and the Node instance itself. This integration ensures that the Node class can utilize the in_ function to perform membership tests, thereby enhancing its functionality. - -**Note**: -- Ensure that the collection y supports the membership test operation. -- The function will raise a TypeError if y is not a collection type that supports the `in` operator. - -**Output Example**: -- If x is 3 and y is [1, 2, 3, 4], the function will return True. -- If x is 'a' and y is 'hello', the function will return False. -## FunctionDef not_in(x, y) -**not_in**: The function of not_in is to determine whether a given element `x` is not present within another collection `y`. - -**parameters**: The parameters of this function. -· x: The element to be checked for non-membership within the collection `y`. -· y: The collection in which the presence of the element `x` is to be checked. - -**Code Description**: The not_in function takes two parameters, `x` and `y`. It evaluates whether the element `x` is not contained within the collection `y`. The function returns a boolean value: `True` if `x` is not in `y`, and `False` if `x` is in `y`. This is achieved using the `not in` operator in Python, which checks for non-membership. - -**Note**: -- The collection `y` can be any iterable, such as a list, tuple, set, or string. -- The function does not modify the input parameters. -- Ensure that `y` is a valid iterable to avoid runtime errors. - -**Output Example**: -- `not_in(3, [1, 2, 4, 5])` returns `True` because 3 is not in the list `[1, 2, 4, 5]`. -- `not_in('a', 'apple')` returns `False` because 'a' is in the string 'apple'. -## FunctionDef getitem(x, index) -**getitem**: The function of getitem is to retrieve an element from a given object `x` using the specified `index`. - -**parameters**: The parameters of this Function. -· x: The object from which an element is to be retrieved. This can be any type that supports indexing, such as lists, tuples, or dictionaries. -· index: The index or key used to access the element within the object `x`. - -**Code Description**: The getitem function is a straightforward implementation of the indexing operation. It takes two parameters: `x` and `index`. The function returns the element of `x` located at the position specified by `index`. This is achieved using the standard indexing syntax `x[index]`. - -In the context of its usage within the project, the getitem function is called by the `__getitem__` method of the `Node` class in the `opto.trace.nodes` module. When the `__getitem__` method is invoked on a `Node` instance with a specific key, it imports the getitem function from the `opto.trace.operators` module and uses it to retrieve the corresponding element from the `Node` instance. This allows for a modular and reusable approach to element retrieval within the project. - -**Note**: -- Ensure that the object `x` supports the indexing operation with the provided `index`. Otherwise, an error will be raised. -- The type of `index` should be compatible with the indexing mechanism of the object `x`. - -**Output Example**: -If `x` is a list `[10, 20, 30]` and `index` is `1`, the return value of `getitem(x, index)` will be `20`. -## FunctionDef pop(x, index) -**pop**: The function of pop is to remove and return an element from a list `x` at the specified `index`. - -**parameters**: The parameters of this Function. -· x: The list from which an element will be removed. -· index: The position of the element to be removed from the list. - -**Code Description**: The `pop` function is designed to operate on a list `x` and remove the element located at the specified `index`. The function utilizes the built-in `pop` method of Python lists, which not only removes the element at the given index but also returns it. This allows the user to both modify the list by removing an element and capture the removed element for further use. The function is straightforward and leverages Python's native list handling capabilities to achieve its purpose efficiently. - -**Note**: -- Ensure that the `index` provided is within the valid range of the list `x`. If the `index` is out of range, a `IndexError` will be raised. -- The list `x` will be modified in place, meaning the original list will be changed after the function call. - -**Output Example**: -If `x = [10, 20, 30, 40]` and `index = 2`, calling `pop(x, index)` will return `30` and modify `x` to `[10, 20, 40]`. -## FunctionDef len_(x) -**len_**: The function of len_ is to return the length of the input object x. - -**parameters**: The parameters of this Function. -· x: Any - The input object whose length is to be calculated. - -**Code Description**: The len_ function is a utility that computes and returns the length of the input object x by leveraging Python's built-in len() function. This function is designed to be a simple wrapper around the built-in len() function, providing a consistent interface for length calculation within the project. - -The function is called by the len method of the Node class in the opto\trace\nodes.py module. When the len method of a Node instance is invoked, it imports the len_ function from the opto.trace.operators module and applies it to the Node instance. This design allows the Node class to utilize the len_ function for determining its length, ensuring modularity and reusability of the len_ function across different parts of the project. - -**Note**: Ensure that the input object x is of a type that supports the len() operation, such as lists, strings, tuples, or other collections. Passing an unsupported type will result in a TypeError. - -**Output Example**: -- If x is a list [1, 2, 3], len_(x) will return 3. -- If x is a string "hello", len_(x) will return 5. -## FunctionDef ord_(x) -**ord_**: The function of ord_ is to return the Unicode number of a character. - -**parameters**: The parameters of this Function. -· x: Any - The character whose Unicode number is to be returned. - -**Code Description**: The ord_ function takes a single parameter, x, which is expected to be a character. It returns the Unicode code point of that character using Python's built-in ord() function. The ord() function is a standard Python function that converts a single character into its corresponding Unicode integer value. This is useful for various applications, such as encoding, decoding, and character manipulation. - -**Note**: -- The input parameter x should be a single character. If x is not a single character, the ord() function will raise a TypeError. -- This function is designed to handle any character that can be represented in Unicode. - -**Output Example**: -- ord_('A') will return 65. -- ord_('€') will return 8364. -## FunctionDef chr_(x) -**chr_**: The function of chr_ is to return the character corresponding to a given Unicode number. - -**parameters**: The parameters of this Function. -· x: A Unicode number (integer) that represents a specific character. - -**Code Description**: The chr_ function takes a single parameter, x, which is expected to be an integer representing a Unicode code point. The function then uses Python's built-in chr() function to convert this Unicode number into its corresponding character. The result is the character that the Unicode number represents. This function is useful for converting numerical Unicode values into their string character equivalents. - -**Note**: -- The input parameter x must be a valid Unicode code point. If x is not a valid Unicode code point, a ValueError will be raised. -- The function does not perform any type checking or validation on the input parameter, so it is the caller's responsibility to ensure that x is a valid integer within the Unicode range. - -**Output Example**: -- chr_(65) will return 'A'. -- chr_(8364) will return '€'. -## FunctionDef concat(x, y) -**concat**: The function of concat is to concatenate two given inputs, x and y. - -**parameters**: The parameters of this Function. -· x: The first input to be concatenated. It can be of any type. -· y: The second input to be concatenated. It can be of any type. - -**Code Description**: The concat function takes two parameters, x and y, and returns their concatenation using the + operator. This function is designed to handle inputs of any type, leveraging Python's dynamic typing and the + operator's ability to concatenate various data types such as strings, lists, and tuples. - -In the context of its usage within the project, the concat function is called by the __add__ method of the Node class in the opto\trace\nodes.py module. When the __add__ method is invoked, it checks the type of the _data attribute of the Node instance. If _data is a string, the concat function is used to concatenate the current Node instance with another Node instance created from the other parameter. This ensures that string concatenation is handled appropriately within the Node class. - -**Note**: -- Ensure that the types of x and y are compatible with the + operator to avoid TypeErrors. -- The behavior of the + operator varies depending on the types of x and y. For example, it concatenates strings and lists but adds numbers. - -**Output Example**: -- If x is "Hello" and y is "World", the return value will be "HelloWorld". -- If x is [1, 2] and y is [3, 4], the return value will be [1, 2, 3, 4]. -- If x is (1, 2) and y is (3, 4), the return value will be (1, 2, 3, 4). -## FunctionDef lower(x) -**lower**: The function of lower is to convert all characters in the input `x` to lower case. - -**parameters**: The parameters of this Function. -· x: Any - The input value that will be converted to lower case. It is expected to be a string or an object that has a `lower()` method. - -**Code Description**: The `lower` function takes a single parameter `x` and returns the result of calling the `lower()` method on `x`. This method is typically available on string objects in Python and converts all uppercase characters in the string to their lowercase counterparts. If `x` is not a string or does not have a `lower()` method, the function will raise an AttributeError. - -**Note**: -- Ensure that the input `x` is a string or an object that implements a `lower()` method to avoid runtime errors. -- This function does not handle non-string inputs that do not have a `lower()` method. - -**Output Example**: -```python -lower("HELLO") # Returns "hello" -lower("Python") # Returns "python" -``` -## FunctionDef upper(x) -**upper**: The function of upper is to convert all characters in the input to upper case. - -**parameters**: The parameters of this Function. -· x: Any - The input value that will be converted to upper case. This can be any type that supports the `upper()` method, typically a string. - -**Code Description**: The `upper` function takes a single parameter `x` and returns the result of calling the `upper()` method on `x`. The `upper()` method is a built-in string method in Python that converts all lowercase letters in a string to uppercase letters. If `x` is not a string or does not support the `upper()` method, the function will raise an AttributeError. - -**Note**: -- Ensure that the input `x` is of a type that supports the `upper()` method, typically a string, to avoid runtime errors. -- This function does not modify the original input but returns a new string with all characters in upper case. - -**Output Example**: -```python -result = upper("hello world") -print(result) # Output: "HELLO WORLD" -``` -## FunctionDef title(x) -**title**: The function of title is to convert the first character of each word in a string to uppercase and the remaining characters to lowercase. - -**parameters**: The parameters of this Function. -· x: Any - The input parameter which is expected to be a string. - -**Code Description**: The title function takes a single parameter, x, which is expected to be a string. It applies the title() method to the string, which capitalizes the first character of each word and converts all other characters to lowercase. This is useful for formatting strings in a standardized way, such as for titles or headings. - -**Note**: -- The input should be a string for the function to work correctly. If the input is not a string, it may result in an AttributeError since the title() method is specific to string objects. -- This function does not handle non-alphabetic characters differently; they will remain unchanged. - -**Output Example**: -If the input string is "hello world", the function will return "Hello World". -If the input string is "PYTHON programming", the function will return "Python Programming". -## FunctionDef swapcase(x) -**swapcase**: The function of swapcase is to swap the case of all characters in the input: converting uppercase characters to lowercase and vice-versa. - -**parameters**: The parameters of this Function. -· x: Any - The input value whose characters' cases are to be swapped. This can be any type that supports the `swapcase` method, typically a string. - -**Code Description**: The swapcase function takes a single parameter `x` and returns a new value where all uppercase characters in `x` are converted to lowercase, and all lowercase characters are converted to uppercase. The function leverages the built-in `swapcase` method available on string-like objects in Python. This method is particularly useful for text processing tasks where case conversion is required. - -**Note**: -- The input `x` must be of a type that supports the `swapcase` method, such as a string. If `x` does not support this method, the function will raise an AttributeError. -- The function does not modify the original input but returns a new value with the cases swapped. - -**Output Example**: -- If the input is `"Hello World"`, the output will be `"hELLO wORLD"`. -- If the input is `"Python3.8"`, the output will be `"pYTHON3.8"`. -## FunctionDef capitalize(x) -**capitalize**: The function of capitalize is to convert the first character of a string to uppercase. - -**parameters**: The parameters of this Function. -· x: Any - The input value that is expected to be a string. - -**Code Description**: The capitalize function takes a single parameter, `x`, which is expected to be a string. It utilizes the built-in `capitalize` method of Python strings to convert the first character of the string to uppercase while leaving the rest of the string unchanged. The function then returns the modified string. If `x` is not a string, the function will raise an AttributeError since the `capitalize` method is not available for non-string types. - -**Note**: -- Ensure that the input `x` is a string to avoid runtime errors. -- This function does not modify the original string but returns a new string with the first character capitalized. - -**Output Example**: -```python -capitalize("hello world") # Returns "Hello world" -capitalize("python") # Returns "Python" -``` -## FunctionDef split(x, y, maxsplit) -**split**: The function of split is to divide a string `x` into parts based on the occurrence of a substring `y`, returning the segments of the string without the substring `y`. - -**parameters**: The parameters of this function. -· x: The main string that needs to be split. -· y: The substring used as the delimiter to split the main string `x`. -· maxsplit: An optional parameter that specifies the maximum number of splits to perform. The default value is -1, which means no limit on the number of splits. - -**Code Description**: The `split` function takes three parameters: `x`, `y`, and `maxsplit`. It utilizes Python's built-in `split` method to divide the string `x` into parts wherever the substring `y` occurs. The `maxsplit` parameter controls the maximum number of splits that can be performed. If `maxsplit` is not provided, or if it is set to -1, the function will split the string at all occurrences of the substring `y`. The function returns a list containing the parts of the string `x` that were separated by the substring `y`. - -**Note**: -- The function will return a list of strings. -- If the substring `y` is not found in the main string `x`, the function will return a list containing the original string `x` as its only element. -- If `maxsplit` is set to 0, the function will return a list containing the original string `x` as its only element, as no splitting will be performed. - -**Output Example**: -```python -split("hello world", " ") -# Output: ['hello', 'world'] - -split("apple,banana,cherry", ",", 1) -# Output: ['apple', 'banana,cherry'] - -split("one,two,three,four", ",", 2) -# Output: ['one', 'two', 'three,four'] - -split("no delimiter here", ",") -# Output: ['no delimiter here'] -``` -## FunctionDef strip(x, chars) -**strip**: The function of strip is to remove the leading and trailing characters from the input `x`. - -**parameters**: The parameters of this function. -· `x`: The input from which leading and trailing characters will be removed. It can be of any type that supports the `strip` method, typically a string. -· `chars`: Optional. A string specifying the set of characters to be removed. If not provided, whitespace characters will be removed by default. - -**Code Description**: The `strip` function is designed to clean up the input `x` by removing any leading and trailing characters specified by the `chars` parameter. If `chars` is not provided, the function defaults to removing whitespace characters. The function leverages the built-in `strip` method available in Python for strings, ensuring efficient and reliable performance. The return value is the cleaned version of `x` with the specified characters removed from both ends. - -**Note**: -- The input `x` must be of a type that supports the `strip` method, such as a string. -- If `chars` is not specified, the function will remove whitespace characters by default. -- This function does not modify the original input but returns a new string with the specified characters removed. - -**Output Example**: -- `strip(" hello ")` returns `"hello"`. -- `strip("##hello##", "#")` returns `"hello"`. -## FunctionDef replace(x, old, new, count) -**replace**: The function of replace is to replace all occurrences of a specified substring within a given string with another substring. - -**parameters**: The parameters of this function. -· x: The original string in which the replacement is to be made. -· old: The substring that needs to be replaced. -· new: The substring that will replace the old substring. -· count: The maximum number of occurrences to replace. If not specified, all occurrences will be replaced. The default value is -1, which means replace all occurrences. - -**Code Description**: The replace function takes four parameters: x, old, new, and count. It utilizes the built-in string method `replace` to substitute all instances of the substring specified by `old` with the substring specified by `new` within the string `x`. The `count` parameter controls the number of replacements to be made. If `count` is set to -1 (the default value), all occurrences of the substring `old` will be replaced by `new`. If `count` is a positive integer, only that many occurrences of `old` will be replaced. - -**Note**: -- The function is case-sensitive, meaning that it will only replace substrings that match the case of `old`. -- If `old` is not found in `x`, the original string `x` will be returned unchanged. -- The `count` parameter must be a non-negative integer or -1. - -**Output Example**: -- replace("hello world", "world", "there") returns "hello there". -- replace("hello world world", "world", "there", 1) returns "hello there world". -- replace("hello world", "WORLD", "there") returns "hello world" (case-sensitive). -## FunctionDef format(x) -**format**: The function of format is to fill in a string template with content using the str.format() method. - -**parameters**: The parameters of this Function. -· x: A string template that contains placeholders to be filled. -· *args: Positional arguments to be used for filling the placeholders in the string template. -· **kwargs: Keyword arguments to be used for filling the placeholders in the string template. - -**Code Description**: The format function takes a string template `x` and fills it with the provided positional (`*args`) and keyword arguments (`**kwargs`). It leverages Python's built-in `str.format()` method to perform this operation. The `str.format()` method allows for complex string formatting operations, including the insertion of variables, formatting of numbers, and more. By passing the arguments and keyword arguments to `x.format(*args, **kwargs)`, the function dynamically replaces the placeholders in the string template with the corresponding values. - -**Note**: -- Ensure that the string template `x` contains valid placeholders that match the provided arguments. -- The function will raise a `KeyError` if a placeholder in the template does not have a corresponding keyword argument. -- The function will raise an `IndexError` if a placeholder in the template does not have a corresponding positional argument. - -**Output Example**: -If the function is called as follows: -```python -format("Hello, {}!", "World") -``` -The return value will be: -```python -"Hello, World!" -``` - -If the function is called with keyword arguments: -```python -format("Hello, {name}!", name="Alice") -``` -The return value will be: -```python -"Hello, Alice!" -``` -## FunctionDef node_getattr(obj, attr) -**node_getattr**: The function of node_getattr is to get the value of the specified attribute from the given object. - -**Parameters**: -- obj: A Node object from which the attribute value is to be retrieved. -- attr: A string representing the name of the attribute to be retrieved. - -**Code Description**: -The `node_getattr` function takes in a `Node` object `obj` and a string `attr` as parameters. It first checks if the `obj` is an instance of a dictionary. If it is, it retrieves the value associated with the `attr` key from the dictionary. Otherwise, it uses the `getattr` function to retrieve the value of the `attr` attribute from the `obj`. - -This function is used in the `getattr` method of the `Node` class in the `opto.trace.nodes.py` module. The `getattr` method is responsible for getting the value of the specified attribute from the `Node` object. It calls the `node_getattr` function passing itself (`self`) and the specified attribute (`key`) as arguments. - -**Note**: -- The `node_getattr` function assumes that the `obj` parameter is a valid `Node` object. -- If the `obj` is not an instance of a dictionary and does not have the specified attribute, a `AttributeError` will be raised. - -**Output Example**: -If `obj` is a dictionary and contains the attribute `attr`, the function will return the value associated with the `attr` key. Otherwise, it will return the value of the `attr` attribute from the `obj`. -## FunctionDef call(fun) -**call**: The function of call is to call the function `fun` with the provided arguments `args` and `kwargs`. - -**parameters**: -- `fun`: A Node object representing the function to be called. -- `*args`: Variable-length argument list. -- `**kwargs`: Keyword arguments. - -**Code Description**: -The `call` function takes a `fun` parameter, which is a Node object representing the function to be called. It also accepts variable-length arguments `args` and keyword arguments `kwargs`. The purpose of this function is to call the function `fun` with the provided arguments. - -First, the function assigns the value of `fun` to a local variable `fun` by accessing the `_data` attribute of the `fun` object. This allows the function to work with the actual function object rather than the Node object. - -Next, the function checks if the `fun` object is callable using the `callable()` function. If it is not callable, an `AssertionError` is raised with the message "The function must be callable." - -Then, the function calls the `fun` function with the provided arguments `args` and keyword arguments `kwargs` using the `*args` and `**kwargs` syntax. The result of the function call is stored in the `output` variable. - -Finally, the function returns the `output` variable. - -**Note**: -- The `fun` parameter must be a callable function. -- The `args` parameter can accept any number of positional arguments. -- The `kwargs` parameter can accept any number of keyword arguments. - -**Output Example**: -If the `fun` function is defined as follows: -```python -def add(a, b): - return a + b -``` -and the `call` function is called with `fun=add` and `args=(2, 3)`, the output will be `5`. diff --git a/generated_docs/opto/trace/propagators/graph_propagator.md b/generated_docs/opto/trace/propagators/graph_propagator.md deleted file mode 100644 index a80d35c4..00000000 --- a/generated_docs/opto/trace/propagators/graph_propagator.md +++ /dev/null @@ -1,166 +0,0 @@ -## ClassDef TraceGraph -**TraceGraph**: The function of TraceGraph is to serve as a feedback container used by the GraphPropagator. It represents a subgraph of nodes and stores user feedback. - -**attributes**: -- graph: A list of Node objects representing the priority queue of nodes in the subgraph. -- user_feedback: Any type of user feedback associated with the TraceGraph. - -**Code Description**: -The TraceGraph class is a feedback container used by the GraphPropagator. It is designed to store a subgraph of nodes and user feedback. The class includes the following methods: - -1. `__add__(self, other)`: This method is used to combine two TraceGraph objects. It checks if either of the user feedbacks is None, and if so, it assigns the non-None user feedback to the resulting TraceGraph. If both user feedbacks are not None, it checks if they are equal and assigns the user feedback to the resulting TraceGraph. The graph is created by merging the two graphs and sorting them based on the priority level. The method returns a new TraceGraph object with the merged graph and user feedback. - -**Note**: -- The TraceGraph class inherits from the AbstractFeedback class, which defines the `__add__` method. -- The `__add__` method ensures that the user feedback is consistent when combining two TraceGraph objects. - -**Output Example**: -```python -TraceGraph(graph=[(1, Node('A')), (2, Node('B'))], user_feedback=None) -``` - -**Reference Relationship**: -- The TraceGraph class is called by the `__add__` method in the TraceGraph class itself. -- The TraceGraph class is utilized in the `node_to_function_feedback` function in the `opto.optimizers.function_optimizer` module. -- The TraceGraph class is also used in the `init_feedback` method of the GraphPropagator class in the `opto.trace.propagators.graph_propagator` module. -### FunctionDef __add__(self, other) -**__add__**: The function of __add__ is to merge two TraceGraph objects while ensuring consistency in user feedback and combining their graphs. - -**parameters**: The parameters of this Function. -· self: The first instance of the TraceGraph object. -· other: The second instance of the TraceGraph object to be added to the first. - -**Code Description**: The __add__ method begins by asserting that at least one of the user_feedback attributes from the two TraceGraph objects is not None. If both user_feedback attributes are None, an assertion error is raised with the message "One of the user feedback should not be None." - -Next, the method determines the user_feedback for the resulting TraceGraph. If one of the user_feedback attributes is None, it uses the non-None user_feedback. If both are not None, it asserts that they are equal, ensuring consistency, and then uses the user_feedback from the first TraceGraph. - -The method then constructs a list of names from the nodes in the other TraceGraph's graph. It creates a complement list by including nodes from the first TraceGraph's graph that do not have names present in the other TraceGraph's graph. This ensures that nodes with the same name are not duplicated. - -Finally, the method merges the complement list and the other TraceGraph's graph using heapq.merge, which merges the lists based on the first element of each tuple (assumed to be a key). The merged list is used to create a new TraceGraph object, which is returned with the combined graph and the determined user_feedback. - -**Note**: -- Ensure that at least one of the TraceGraph objects has a non-None user_feedback before using the __add__ method. -- If both TraceGraph objects have user_feedback, they must be identical to avoid an assertion error. - -**Output Example**: -Assuming TraceGraph objects `tg1` and `tg2` are being added: -```python -tg1 + tg2 -``` -This would return a new TraceGraph object with a combined graph and consistent user_feedback. -*** -## ClassDef GraphPropagator -**GraphPropagator**: The GraphPropagator class is a subclass of the Propagator class. It provides specific implementations for the `init_feedback` and `_propagate` methods, as well as an `aggregate` method. The purpose of this class is to collect all the nodes seen in the path and compute the propagated feedback to the parent nodes based on the child node's description, data, and feedback. - -**attributes**: -- None - -**Code Description**: -- The `init_feedback` method takes two parameters: `node` (the current node) and `feedback` (the user feedback). It returns a TraceGraph object that represents the initial feedback for the given node. The TraceGraph object is created using the TraceGraph class and initialized with the current node and the user feedback. - -- The `_propagate` method takes a `child` parameter of type `MessageNode` and computes the propagated feedback to the parent nodes based on the child node's description, data, and feedback. It first creates a list of tuples representing the parents of the child node. Each tuple contains the level of the parent node and the parent node itself. Then, it aggregates the feedback from the child node and creates a TraceGraph object using the TraceGraph class. The aggregated feedback is computed by adding the feedback from the child node to a TraceGraph object that represents the parents of the child node. The external dependencies on parameters not visible in the current graph level are also included in the feedback. Finally, the method returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. - -- The `aggregate` method takes a `feedback` parameter of type `Dict[Node, List[TraceGraph]]` and aggregates the feedback from multiple children. It first checks that the length of each value in the feedback dictionary is 1 and that each value is an instance of the TraceGraph class. Then, it sums the feedback values and returns the aggregated feedback as a TraceGraph object. - -**Note**: -- The `init_feedback` and `_propagate` methods are specific implementations of abstract methods defined in the Propagator class. -- The `aggregate` method is a helper method used by the `_propagate` method to aggregate feedback from multiple children. - -**Output Example**: -Given a properly implemented GraphPropagator object, the return value of the `_propagate` method might look like the following: -```python -{ - parent_node_1: feedback_data_1, - parent_node_2: feedback_data_2, - # ... other parent nodes and their respective feedback -} -``` -### FunctionDef init_feedback(self, node, feedback) -**init_feedback**: The function of init_feedback is to initialize feedback for a given node in the GraphPropagator. - -**parameters**: -- node: The node for which feedback is being initialized. -- feedback: The user feedback associated with the node. - -**Code Description**: -The init_feedback function is a method of the GraphPropagator class in the opto.trace.propagators.graph_propagator module. It is used to initialize feedback for a given node in the graph propagation process. The function takes two parameters: the node for which feedback is being initialized and the user feedback associated with the node. - -Inside the function, a TraceGraph object is created using the TraceGraph class. The TraceGraph object is initialized with a graph containing a single tuple representing the level of the node and the node itself. The user feedback is also assigned to the TraceGraph object. - -The TraceGraph object is then returned as the output of the init_feedback function. - -**Reference Relationship**: -- The init_feedback function is called by the backward method in the Node class in the opto.trace.nodes module. -- The init_feedback function is called by the propagate method in the GraphPropagator class in the opto.trace.propagators.graph_propagator module. - -**Note**: It is important to ensure that the node and feedback parameters are properly provided when calling the init_feedback function to avoid potential issues. - -**Output Example**: -If the node parameter is a Node object representing a node with level 2 and the feedback parameter is "Good job!", calling the init_feedback function will return a TraceGraph object with the following attributes: -- graph: [(2, Node)] -- user_feedback: "Good job!" -*** -### FunctionDef _propagate(self, child) -**_propagate**: The function of _propagate is to propagate feedback from a child node to its parent nodes in the graph. - -**parameters**: -- self: The current object. -- child: The child node from which the feedback is propagated. - -**Code Description**: -The `_propagate` function is a method of the `GraphPropagator` class in the `graph_propagator.py` module. It takes in the current object (`self`) and a child node (`child`) as parameters. The function first creates a list called `graph` by iterating over the parents of the child node and storing them along with their priority level. The priority level is determined by the `level` attribute of each parent node. The `graph` list represents the parents of the child node. - -Next, the function aggregates the feedback from the child node by calling the `aggregate` method of the current object (`self`). The `aggregate` method takes in the feedback from multiple children nodes and returns the aggregated feedback as a `TraceGraph` object. The feedback is obtained from the `feedback` attribute of the child node. - -The function then asserts that the aggregated feedback is an instance of the `TraceGraph` class. This ensures that the feedback is in the correct format. - -After that, the function iterates over the external dependencies of the child node and adds the feedback to each external dependency by calling the `_add_feedback` method of the external dependency node. This ensures that the feedback is correctly propagated to the external dependencies. - -Finally, the function returns a dictionary comprehension that maps each parent node to the aggregated feedback. - -The `_propagate` function is an essential part of the graph propagation process in the `GraphPropagator` class. It is responsible for propagating feedback from a child node to its parent nodes, ensuring that the feedback flows correctly through the graph structure. - -**Note**: -- The function assumes that the child node has a `parents` attribute that returns a list of parent nodes. -- The function assumes that the child node has an `external_dependencies` attribute that returns a set of external dependency nodes. -- The function assumes that the child node has a `feedback` attribute that contains the feedback from the child node. -- The function assumes that the feedback can be aggregated using the `aggregate` method. -- The function assumes that the external dependencies have a `_add_feedback` method to add the feedback from the child node. -- The function returns a dictionary that maps each parent node to the aggregated feedback. - -**Output Example**: -If the child node has two parents, the `_propagate` function will return a dictionary with two key-value pairs, where each key represents a parent node and the corresponding value represents the aggregated feedback from the child node. -```python -{ - parent_node1: aggregated_feedback1, - parent_node2: aggregated_feedback2 -} -``` -*** -### FunctionDef aggregate(self, feedback) -**aggregate**: The function of aggregate is to aggregate feedback from multiple children. - -**Parameters**: -- feedback: A dictionary that maps a Node to a list of TraceGraph objects representing the feedback from the child nodes. - -**Code Description**: -The `aggregate` function takes in a dictionary of feedback from multiple children. It first checks that each child has provided exactly one feedback and that the feedback is of type TraceGraph. Then, it calculates the sum of the feedback values for each child and stores them in a list called `values`. If the length of `values` is zero, indicating that there is no feedback, it returns a TraceGraph object with an empty graph and a user_feedback attribute set to None. Otherwise, it returns the sum of the values. - -This function is used to aggregate the feedback received from multiple children nodes. It ensures that the feedback is valid and performs the aggregation by summing the feedback values. The resulting aggregated feedback is returned as a TraceGraph object. - -**Reference Relationship**: -- This function is called by the `summarize` method in the `FunctionOptimizer` class in the `opto.optimizers.function_optimizer` module. -- This function is also called by the `_propagate` method in the `GraphPropagator` class in the `opto.trace.propagators.graph_propagator` module. - -**Note**: -- The feedback dictionary should contain exactly one feedback value for each child node. -- The feedback values should be of type TraceGraph. -- The function assumes that the feedback values can be summed. -- If there is no feedback, an empty TraceGraph object is returned. -- The function does not modify the input feedback dictionary. - -**Output Example**: -```python -TraceGraph(graph=[(1, Node('A')), (2, Node('B'))], user_feedback=None) -``` -*** diff --git a/generated_docs/opto/trace/propagators/propagators.md b/generated_docs/opto/trace/propagators/propagators.md deleted file mode 100644 index 3a080c74..00000000 --- a/generated_docs/opto/trace/propagators/propagators.md +++ /dev/null @@ -1,338 +0,0 @@ -## ClassDef AbstractPropagator -**AbstractPropagator**: The function of AbstractPropagator is to serve as a base class for propagating feedback from a child node to its parent nodes in a hierarchical structure. - -**attributes**: The attributes of this Class. -· This class does not define any attributes directly. - -**Code Description**: The AbstractPropagator class is designed to facilitate the propagation of feedback from a child node to its parent nodes. It provides a structured way to ensure that feedback is correctly propagated and formatted. - -- The `__call__` method is the primary interface for propagating feedback. When this method is called with a `MessageNode` instance as the `child` parameter, it performs several checks and operations: - - It asserts that the `child` is an instance of `MessageNode`. - - It ensures that all feedback values in the `child` node have a length of at most 1. - - It calls the `propagate` method to compute the propagated feedback. - - It verifies that the propagated feedback is a dictionary where the keys are the parent nodes and the values are the feedback. - - Finally, it returns the propagated feedback. - -- The `propagate` method is an abstract method that must be implemented by subclasses. It is responsible for computing the propagated feedback to the parent nodes of the given `child` node. The method should return a dictionary where the keys are the parent nodes and the values are the propagated feedback. Since this method is not implemented in the AbstractPropagator class, it raises a `NotImplementedError`. - -The AbstractPropagator class is extended by the `Propagator` class, which provides specific implementations for the `propagate` method and additional functionalities such as registering custom propagation functions and initializing feedback. - -**Note**: -- The `propagate` method must be implemented in any subclass of AbstractPropagator. -- The `__call__` method ensures that the feedback is correctly formatted and propagated, making it a critical part of the feedback propagation process. - -**Output Example**: -Given a properly implemented subclass of AbstractPropagator, the return value of the `__call__` method might look like the following: -```python -{ - parent_node_1: feedback_data_1, - parent_node_2: feedback_data_2, - # ... other parent nodes and their respective feedback -} -``` -This dictionary maps parent nodes to their respective propagated feedback. -### FunctionDef __call__(self, child) -**__call__**: The function of __call__ is to propagate the feedback from a child node to its parents. -**parameters**: -- child: A MessageNode object representing the child node for which the feedback needs to be propagated. - -**Code Description**: -The `__call__` function is a method of the `AbstractPropagator` class defined in the `propagators.py` module. It is responsible for propagating the feedback from a child node to its parents. The function takes a `child` parameter, which is expected to be a `MessageNode` object. - -The function first checks if the `child` is an instance of `MessageNode` and if the feedback from the child is of the correct format. The feedback should be a dictionary with the parents of the child as keys and the feedback values as values. - -Next, the function calls the `propagate` method of the concrete propagator class that inherits from `AbstractPropagator`. This method is expected to be implemented in the concrete propagator class and should perform the actual propagation of feedback. The `propagate` method returns the propagated feedback as a dictionary. - -The function then checks if the propagated feedback has the correct format, ensuring that it is a dictionary and that all the parents of the child are present as keys in the dictionary. - -Finally, the function returns the propagated feedback. - -**Note**: -- The `__call__` function is expected to be implemented in a concrete propagator class that inherits from `AbstractPropagator`. -- The `__call__` function assumes that the feedback from the child is already computed and stored in the `feedback` attribute of the child node. -- The function raises an error if the child is not an instance of `MessageNode` or if the feedback from the child is not of the correct format. - -**Output Example**: A possible appearance of the code's return value could be: -``` -{ - parent_node_1: feedback_value_1, - parent_node_2: feedback_value_2, - ... -} -``` -This example assumes that the propagated feedback is a dictionary with the parent nodes as keys and the corresponding feedback values as values. The actual content of the feedback will depend on the specific implementation and use case within the project. -*** -### FunctionDef propagate(self, child) -**propagate**: The function of propagate is to compute and return the propagated feedback to the parents of a given node. It returns a dictionary where the keys are the parents and the values are the propagated feedback. - -**parameters**: -- child: A MessageNode object representing the child node for which the feedback needs to be propagated. - -**Code Description**: -The `propagate` function is a method of the `AbstractPropagator` class defined in the `propagators.py` module. It is responsible for propagating the feedback from a child node to its parents. The function takes a `child` parameter, which is expected to be a `MessageNode` object. - -The function first checks if the `child` is an instance of `MessageNode` and if the feedback from the child is of the correct format. The feedback should be a dictionary with the parents of the child as keys and the feedback values as values. - -Next, the function calls the `propagate` method of the concrete propagator class that inherits from `AbstractPropagator`. This method is expected to be implemented in the concrete propagator class and should perform the actual propagation of feedback. The `propagate` method returns the propagated feedback as a dictionary. - -The function then checks if the propagated feedback has the correct format, ensuring that it is a dictionary and that all the parents of the child are present as keys in the dictionary. - -Finally, the function returns the propagated feedback. - -**Note**: -- The `propagate` function is expected to be implemented in a concrete propagator class that inherits from `AbstractPropagator`. -- The `propagate` function assumes that the feedback from the child is already computed and stored in the `feedback` attribute of the child node. -- The function raises an error if the child is not an instance of `MessageNode` or if the feedback from the child is not of the correct format. -*** -## ClassDef AbstractFeedback -**AbstractFeedback**: The function of AbstractFeedback is to serve as a feedback container used by propagators, supporting addition operations. - -**attributes**: This class does not define any attributes. - -**Code Description**: -The AbstractFeedback class is designed to act as a base class for feedback containers used by propagators. It defines the necessary interface for feedback objects that need to support addition operations. The class includes two methods: - -1. `__add__(self, other)`: This method is intended to handle the addition of two feedback objects. However, it raises a NotImplementedError, indicating that any subclass must implement this method to define the specific addition behavior. - -2. `__radd__(self, other)`: This method supports the addition operation when the AbstractFeedback object is on the right-hand side of the addition. It checks if the other operand is zero, which is useful for operations like sum where the initial value is zero. If the other operand is zero, it returns the current object (self). Otherwise, it delegates the addition operation to the `__add__` method. - -The AbstractFeedback class is utilized in the TraceGraph class, which inherits from AbstractFeedback. The TraceGraph class provides a concrete implementation of the `__add__` method, ensuring that feedback objects can be combined according to specific rules defined within TraceGraph. This relationship indicates that AbstractFeedback serves as a foundational component for more specialized feedback containers like TraceGraph. - -**Note**: -- Any subclass of AbstractFeedback must implement the `__add__` method to define how feedback objects should be combined. -- The `__radd__` method facilitates the use of AbstractFeedback objects in operations like sum, where the initial value might be zero. - -**Output Example**: -Since AbstractFeedback is an abstract class and does not implement the `__add__` method, it does not produce any direct output. However, a subclass like TraceGraph would produce combined feedback objects when the `__add__` method is called. For example, combining two TraceGraph objects might result in a new TraceGraph object with a merged graph and user feedback. -### FunctionDef __add__(self, other) -**__add__**: The function of __add__ is to define the addition operation for instances of the class. - -**parameters**: The parameters of this Function. -· self: The instance of the class on which the method is called. -· other: The instance or value to be added to the instance represented by self. - -**Code Description**: The __add__ method is intended to define the behavior of the addition operation for instances of the class it belongs to. However, in its current implementation, it raises a NotImplementedError, indicating that the addition operation is not yet implemented for this class. This method is crucial for enabling the use of the '+' operator with instances of the class. - -The __add__ method is also indirectly called by the __radd__ method within the same class. The __radd__ method is designed to handle the addition operation when the instance appears on the right-hand side of the '+' operator. If the other operand is zero, __radd__ returns the instance itself, supporting the use of the sum function. Otherwise, it delegates the addition operation to the __add__ method. - -**Note**: -- The __add__ method currently raises a NotImplementedError, so attempting to use the '+' operator with instances of this class will result in an error. -- To enable addition, the __add__ method needs to be properly implemented. -- The __radd__ method relies on __add__ for non-zero operands, so both methods should be considered together when implementing addition functionality. -*** -### FunctionDef __radd__(self, other) -**__radd__**: The function of __radd__ is to handle the addition operation when the instance appears on the right-hand side of the '+' operator. - -**parameters**: The parameters of this Function. -· self: The instance of the class on which the method is called. -· other: The instance or value to be added to the instance represented by self. - -**Code Description**: The __radd__ method is designed to support the addition operation when the instance of the class appears on the right-hand side of the '+' operator. This method is particularly useful for enabling the use of the sum function with instances of the class. When the other operand is zero, __radd__ returns the instance itself, ensuring that the sum function can correctly handle the initial zero value. If the other operand is not zero, the method delegates the addition operation to the __add__ method of the class. - -The __add__ method, which is called by __radd__ for non-zero operands, is intended to define the behavior of the addition operation for instances of the class. However, in its current implementation, __add__ raises a NotImplementedError, indicating that the addition operation is not yet implemented for this class. Therefore, to fully enable addition functionality, the __add__ method needs to be properly implemented. - -**Note**: -- The __add__ method currently raises a NotImplementedError, so attempting to use the '+' operator with instances of this class will result in an error. -- The __radd__ method relies on __add__ for non-zero operands, so both methods should be considered together when implementing addition functionality. - -**Output Example**: -- If `other` is 0, the method returns the instance itself. -- If `other` is not 0, the method attempts to return the result of `self.__add__(other)`, which currently raises a NotImplementedError. -*** -## ClassDef Propagator -**Propagator**: The function of Propagator is to propagate feedback from a child node to its parent nodes based on the provided rules and functions. - -**attributes**: The attributes of this Class. -- `override`: A dictionary that stores the override propagate functions for specific operator names. - -**Code Description**: The Propagator class is a subclass of the AbstractPropagator class. It provides specific implementations for the `propagate` and `_propagate` methods, as well as additional functionalities such as registering custom propagation functions and initializing feedback. - -- The `register` method allows users to register a custom propagate function for a specific operator name. It takes two parameters: `operator_name` (the name of the operator) and `propagate_function` (the custom propagate function). It adds the `operator_name` and `propagate_function` to the `override` dictionary. - -- The `propagate` method is responsible for computing the propagated feedback to the parent nodes of the given `child` node. It takes a `child` parameter of type `MessageNode` and returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. It first retrieves the operator name from the `child` node using the `get_op_name` function. If the operator name is found in the `override` dictionary, it calls the corresponding propagate function with the `child` node as the argument. Otherwise, it calls the `_propagate` method to compute the propagated feedback. - -- The `init_feedback` method is an abstract method that must be implemented by subclasses. It takes a `feedback` parameter and returns the initialized feedback object that will be propagated recursively. Since this method is not implemented in the Propagator class, it raises a `NotImplementedError` if called. - -- The `_propagate` method is a protected method that computes the propagated feedback to the parent nodes based on the `child` node's description, data, and feedback. It takes a `child` parameter of type `MessageNode` and returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. It first creates a list of tuples representing the parents of the `child` node. Then, it aggregates the feedback from the `child` node and creates a `TraceGraph` object. It also adds the external dependencies on parameters not visible in the current graph level. Finally, it returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. - -**Note**: -- The `propagate` method must be implemented in any subclass of Propagator. -- The `init_feedback` and `_propagate` methods are abstract methods and must be implemented in subclasses. -- The `register` method allows users to register custom propagate functions for specific operator names, providing flexibility in the feedback propagation process. - -**Output Example**: -Given a properly implemented subclass of Propagator, the return value of the `propagate` method might look like the following: -```python -{ - parent_node_1: feedback_data_1, - parent_node_2: feedback_data_2, - # ... other parent nodes and their respective feedback -} -``` -This dictionary maps parent nodes to their respective propagated feedback. -### FunctionDef __init__(self) -**__init__**: The function of __init__ is to initialize an instance of the Propagator class. - -**parameters**: The parameters of this Function. -· This function does not take any parameters. - -**Code Description**: The __init__ function is a constructor method for the Propagator class. When an instance of the Propagator class is created, this method is automatically called to set up the initial state of the object. Specifically, it initializes an instance variable named `override` as an empty dictionary. This dictionary is intended to store override propagation functions, where the keys are operator names and the values are the corresponding override functions. This setup allows for flexible and dynamic modification of propagation behavior based on specific operators. - -**Note**: -- The `override` dictionary is initially empty and can be populated later with operator names and their corresponding override functions. -- This method does not require any arguments and does not return any values. -- Proper management of the `override` dictionary is essential for ensuring the correct propagation behavior in the Propagator class. -*** -### FunctionDef register(self, operator_name, propagate_function) -**register**: The function of register is to associate a given operator name with a specific propagation function. - -**parameters**: The parameters of this Function. -· operator_name: The name of the operator to be registered. -· propagate_function: The function that defines how the operator should propagate. - -**Code Description**: The register function is a method designed to add or override an entry in the `override` dictionary of the Propagator class. When called, it takes two arguments: `operator_name` and `propagate_function`. The `operator_name` is a string that identifies the operator, and `propagate_function` is a callable that defines the behavior of the operator during propagation. The method assigns the `propagate_function` to the `operator_name` key in the `override` dictionary, effectively registering or updating the propagation behavior for that operator. - -**Note**: -- Ensure that `operator_name` is unique within the context of the `override` dictionary to avoid unintentional overwrites. -- The `propagate_function` should be a valid callable that adheres to the expected signature and behavior required by the Propagator class. -*** -### FunctionDef propagate(self, child) -**propagate**: The function of propagate is to compute and return the propagated feedback to the parents of a given MessageNode based on the node's description, data, and feedback. - -**parameters**: -- child: A MessageNode object representing the child node for which the feedback needs to be propagated. - -**Code Description**: -The `propagate` function is a method of the `Propagator` class. It takes a child `MessageNode` as input and computes the propagated feedback to its parents. The function first checks if there is an override function defined for the operator associated with the child's description. If an override function is defined, it is called to compute the propagated feedback. Otherwise, the default `_propagate` function is called. - -The purpose of the `propagate` function is to compute the propagated feedback from a child `MessageNode` to its parents. The feedback is computed based on the child's description, data, and feedback. The function returns a dictionary where the keys are the parents of the child and the values are the propagated feedback. - -The `propagate` function provides a way to customize the propagation behavior for different types of operators. By defining an override function for a specific operator, developers can specify how the feedback should be propagated for that operator. This allows for flexibility and customization in the propagation process. - -It is important to note that the `propagate` function relies on the `_propagate` function, which is a placeholder and needs to be implemented in a subclass of the `Propagator` class. The implementation of the `_propagate` function will depend on the specific requirements of the operator being propagated. The `_propagate` function raises a `NotImplementedError` to indicate that it needs to be implemented. - -The `propagate` function is called by other parts of the project to propagate feedback from child nodes to parent nodes. It is an essential component of the graph propagation process and plays a crucial role in updating the values of parent nodes based on the feedback received from their child nodes. - -**Note**: -- The `_propagate` function is a placeholder and needs to be implemented in a subclass of the `Propagator` class. -- The `propagate` function provides a way to customize the propagation behavior for different types of operators. -- The implementation of the `_propagate` function will depend on the specific requirements of the operator being propagated. -- The `propagate` function is an essential component of the graph propagation process and plays a crucial role in updating the values of parent nodes based on the feedback received from their child nodes. - -**Output Example**: -If the `propagate` function is called with a child `MessageNode` object and the feedback is successfully propagated to its parents, the function will return a dictionary where the keys are the parent nodes and the values are the propagated feedback. -*** -### FunctionDef init_feedback(self, feedback) -**init_feedback**: The function of init_feedback is to create a feedback object from raw feedback that will be propagated recursively. - -**parameters**: The parameters of this Function. -· feedback: Raw feedback of any type that needs to be processed into a feedback object. - -**Code Description**: The init_feedback function is designed to take raw feedback as input and transform it into a feedback object that can be propagated recursively through a system. This function is essential for initializing the feedback mechanism in a propagation process. The function is currently not implemented and raises a NotImplementedError, indicating that it is intended to be overridden in a subclass or implemented later. - -In the context of its usage within the project, init_feedback is called by the backward method of the Node class in opto\trace\nodes.py. The backward method is responsible for performing a backward pass through a graph of nodes, propagating feedback from child nodes to parent nodes. During this process, init_feedback is used to initialize the feedback for the current node before it is propagated to its parents. This ensures that the feedback is in the correct format and ready for recursive propagation. - -**Note**: -- The init_feedback function must be implemented before it can be used effectively. -- It is crucial to ensure that the feedback object created by this function is compatible with the propagation mechanism used in the backward method. -- Proper implementation of this function is necessary to avoid runtime errors and ensure the correct functioning of the feedback propagation process. -*** -### FunctionDef _propagate(self, child) -**_propagate**: The function of _propagate is to compute and return the propagated feedback to the parents of a given MessageNode based on the node's description, data, and feedback. - -**parameters**: -- self: The instance of the Propagator class. -- child: The MessageNode for which the feedback needs to be propagated. - -**Code Description**: -The _propagate function is a method of the Propagator class. It takes a child MessageNode as input and computes the propagated feedback to its parents. The function first checks if there is an override function defined for the operator associated with the child's description. If an override function is defined, it is called to compute the propagated feedback. Otherwise, the default _propagate function is called. - -The _propagate function raises a NotImplementedError, indicating that it needs to be implemented in a subclass of the Propagator class. This allows for customization of the propagation behavior for different types of operators. - -The purpose of the _propagate function is to compute the propagated feedback from a child MessageNode to its parents. The feedback is computed based on the child's description, data, and feedback. The function returns a dictionary where the keys are the parents of the child and the values are the propagated feedback. - -It is important to note that the _propagate function is a placeholder and needs to be implemented in a subclass of the Propagator class. The implementation of this function will depend on the specific requirements of the operator being propagated. - -**Note**: -- The _propagate function is a placeholder and needs to be implemented in a subclass of the Propagator class. -- The function raises a NotImplementedError to indicate that it needs to be implemented. -- The implementation of the _propagate function will depend on the specific requirements of the operator being propagated. -*** -## ClassDef SumPropagator -**SumPropagator**: The function of SumPropagator is to propagate feedback from a child node to its parent nodes by summing the feedback values. - -**attributes**: The attributes of this Class. -- This class does not define any additional attributes beyond those inherited from the Propagator class. - -**Code Description**: The SumPropagator class is a subclass of the Propagator class. It provides specific implementations for the `init_feedback` and `_propagate` methods, which are abstract methods in the Propagator class. - -- The `init_feedback` method takes a `feedback` parameter of any type and returns it as-is. This method is used to initialize the feedback object that will be propagated recursively. - -- The `_propagate` method is responsible for computing the propagated feedback to the parent nodes of the given `child` node. It takes a `child` parameter of type `MessageNode` and returns a dictionary where the keys are the parent nodes and the values are the propagated feedback. - - - If the `child` node's feedback contains a "user" key, it asserts that the "user" feedback is the only feedback and that it contains exactly one item. It then extracts this feedback item. - - - If the "user" key is not present, it sums the feedback values from all keys in the `child` node's feedback. It asserts that the feedback list is not empty and that all feedback items are of the same type. If the feedback items are strings, it concatenates them; otherwise, it sums them numerically. - - - Finally, it returns a dictionary where each parent node of the `child` node is mapped to the computed feedback. - -The SumPropagator class is used within the context of the opto.trace.propagators module, which deals with propagating feedback in a hierarchical structure of nodes. It overrides the abstract methods of the Propagator class to provide a specific feedback propagation mechanism based on summing feedback values. - -**Note**: -- The `init_feedback` method in SumPropagator simply returns the input feedback without any modifications. -- The `_propagate` method ensures that feedback values are either concatenated (if they are strings) or summed (if they are numeric), and it performs type checks to ensure consistency. - -**Output Example**: -Given a `child` node with feedback and parent nodes, the return value of the `_propagate` method might look like the following: -```python -{ - parent_node_1: summed_feedback, - parent_node_2: summed_feedback, - # ... other parent nodes and their respective feedback -} -``` -This dictionary maps parent nodes to their respective propagated feedback, which is the sum of the feedback values from the `child` node. -### FunctionDef init_feedback(self, feedback) -**init_feedback**: The function of init_feedback is to initialize and return the provided feedback. - -**parameters**: The parameters of this Function. -· feedback: Any type of input that represents the feedback to be initialized. - -**Code Description**: The init_feedback function is designed to take a single parameter, feedback, which can be of any type. The function simply returns the feedback parameter as it is. This implies that the function's primary purpose is to serve as a placeholder or a pass-through for the feedback data, potentially for further processing or storage within the context of the SumPropagator class. - -**Note**: -- The function does not perform any validation or transformation on the feedback parameter. -- Ensure that the feedback parameter is provided in the expected format and type as required by the broader application context. - -**Output Example**: -If the feedback parameter is provided as a string "Positive feedback", the function will return "Positive feedback". -If the feedback parameter is provided as a dictionary {"score": 10, "comment": "Excellent"}, the function will return {"score": 10, "comment": "Excellent"}. -*** -### FunctionDef _propagate(self, child) -**_propagate**: The function of _propagate is to calculate the feedback value for a given child node and propagate it to its parent nodes. -**parameters**: -- self: The current object. -- child: The child node for which the feedback needs to be propagated. -**Code Description**: -The `_propagate` function is a method of the `SumPropagator` class in the `propagators.py` module. It takes two parameters, `self` and `child`, where `self` refers to the current `SumPropagator` object and `child` is the child node for which the feedback needs to be propagated. - -The function first checks if the child node has a feedback entry for the "user" key. If it does, it assumes that the user feedback is the only feedback and assigns it to the `feedback` variable. Otherwise, it sums up the feedback values from all the keys in the `child.feedback` dictionary and assigns it to the `feedback_list` variable. - -Next, the function performs some assertions to ensure the validity of the feedback data. It checks if the `feedback_list` has at least one element and if all the elements in the list have the same type. If the elements are of type string, it concatenates them using the `"".join()` method and assigns the result to the `feedback` variable. Otherwise, it calculates the sum of the elements using the `sum()` function and assigns it to the `feedback` variable. - -Finally, the function creates a dictionary comprehension to map each parent node of the child node to the calculated feedback value. The parent nodes are obtained by calling the `parents()` function of the child node. - -The `_propagate` function is an important part of the feedback propagation process in the graph structure. It ensures that the feedback from a child node is correctly calculated and propagated to its parent nodes. This is crucial for updating the parameters and optimizing the graph based on the feedback received. - -**Note**: The `_propagate` function assumes that the feedback data is stored in the `child.feedback` dictionary, where the keys represent different sources of feedback and the values represent the corresponding feedback values. The function handles two scenarios: when there is only user feedback available and when there are multiple feedback sources that need to be summed up. It is important to ensure that the feedback data is correctly formatted and consistent with the expectations of the function. - -**Output Example**: A possible appearance of the code's return value could be: -``` -{ - parent_node_1: feedback_value_1, - parent_node_2: feedback_value_2, - ... -} -``` -This example assumes that the `child.parents` attribute contains a list of parent nodes and the `feedback` variable contains the calculated feedback value for each parent node. The actual structure and content of the return value will depend on the specific implementation and use case within the project. -*** diff --git a/generated_docs/opto/trace/trace.md b/generated_docs/opto/trace/trace.md deleted file mode 100644 index f0e43488..00000000 --- a/generated_docs/opto/trace/trace.md +++ /dev/null @@ -1,43 +0,0 @@ -## ClassDef stop_tracing -**stop_tracing**: The function of stop_tracing is to disable tracing within a specific context. - -**attributes**: -- None - -**Code Description**: -The `stop_tracing` class is a context manager that is used to disable tracing within a specific context. When the `stop_tracing` object is entered, it sets the `GRAPH.TRACE` attribute to `False`, effectively disabling tracing. When the context is exited, the `GRAPH.TRACE` attribute is set back to `True`, enabling tracing again. - -This class is typically used in conjunction with the `trace` module to control the tracing behavior of a program. Tracing is a technique used to monitor the execution of a program by recording information about each executed statement. By disabling tracing within a specific context, developers can exclude certain parts of the code from being traced, which can be useful for performance optimization or debugging purposes. - -In the project, the `stop_tracing` class is called in the `test_bundle.py` file within the `run` function. It is used to disable tracing while executing certain code blocks. This allows developers to selectively trace or exclude specific parts of the code during testing. - -**Note**: -- The `stop_tracing` class is a context manager, so it should be used within a `with` statement to ensure proper entry and exit. -- Disabling tracing can be useful for performance optimization or debugging purposes, but it should be used with caution as it may affect the accuracy of the tracing results. -### FunctionDef __enter__(self) -**__enter__**: The function of __enter__ is to set the tracing state to False. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the class that contains this method. - -**Code Description**: The __enter__ method is a special method used in the context management protocol in Python. When an instance of the class containing this method is used in a `with` statement, the __enter__ method is automatically invoked at the beginning of the block. In this specific implementation, the __enter__ method sets the `TRACE` attribute of the `GRAPH` object to `False`. This action effectively stops or disables tracing within the context of the `with` statement. The `GRAPH` object is assumed to be a global or otherwise accessible object that controls tracing functionality. - -**Note**: -- Ensure that the `GRAPH` object and its `TRACE` attribute are properly defined and accessible within the scope where this method is used. -- This method is typically used in conjunction with the `__exit__` method to manage resources or states within a `with` statement. -*** -### FunctionDef __exit__(self, type, value, traceback) -**__exit__**: The function of __exit__ is to reset the tracing state by setting `GRAPH.TRACE` to `True`. - -**parameters**: The parameters of this Function. -· type: The exception type, if any exception was raised. -· value: The exception instance, if any exception was raised. -· traceback: The traceback object, if any exception was raised. - -**Code Description**: The `__exit__` method is a special method used in context management in Python. It is called when the execution of a block inside a `with` statement is finished. In this specific implementation, the `__exit__` method sets the `TRACE` attribute of the `GRAPH` object to `True`. This indicates that tracing should be enabled or resumed after the context block is exited, regardless of whether an exception was raised or not. The method takes three parameters: `type`, `value`, and `traceback`, which are standard for the `__exit__` method and provide information about any exception that may have occurred within the `with` block. - -**Note**: -- This method is part of the context management protocol and is automatically invoked at the end of a `with` statement. -- The parameters `type`, `value`, and `traceback` are necessary for handling exceptions, but in this implementation, they are not used. -- Ensure that `GRAPH` and its `TRACE` attribute are properly defined and accessible within the scope where this `__exit__` method is used. -*** diff --git a/generated_docs/opto/trace/utils.md b/generated_docs/opto/trace/utils.md deleted file mode 100644 index d5b4099a..00000000 --- a/generated_docs/opto/trace/utils.md +++ /dev/null @@ -1,320 +0,0 @@ -## FunctionDef contain(container_of_nodes, node) -**contain**: The function of contain is to check if a given node is present in a container of nodes. -**parameters**: -- container_of_nodes: A container (such as a list or set) that holds nodes. -- node: The node to be checked for presence in the container. -**Code Description**: -The `contain` function takes in a container of nodes and a node as parameters. It uses a list comprehension to iterate over the container and checks if the given node is identical (using the `is` operator) to any of the nodes in the container. The function returns `True` if the node is found in the container, and `False` otherwise. - -This function is used in various parts of the project. In the `opto\trace\bundle.py/FunModule/forward` function, the `contain` function is called to check if a node is present in the `container_of_nodes` list. It is used to determine the external dependencies of the operator function. - -In the `opto\trace\utils.py/MinHeap/__contains__` function, the `contain` function is used to check if an item is present in the `self.heap` list. - -The `contain` function is also used in the `tests\unit_tests\test_bundle.py/run` function to check if a node is present in a container of nodes. - -**Note**: The `contain` function checks for identity (using the `is` operator) instead of value equality. This means that it will only return `True` if the node is the exact same object in memory as one of the nodes in the container. - -**Output Example**: -```python -container_of_nodes = [node(1), node(2), node(3)] -node = node(2) -print(contain(container_of_nodes, node)) -# Output: True -``` -## FunctionDef parse_eqs_to_dict(text) -**parse_eqs_to_dict**: The function of parse_eqs_to_dict is to parse a given text containing equations into a dictionary. - -**parameters**: The parameters of this Function. -· text: A string containing equations separated by new lines. Each equation should be in the format `key=value`. - -**Code Description**: The parse_eqs_to_dict function processes a string of equations and converts it into a dictionary where each key-value pair represents an equation. The function follows these steps: - -1. **Splitting the Input Text**: The input text is split into individual lines using the newline character (`\n`) as the delimiter. -2. **Initialization**: An empty dictionary `result_dict` is initialized to store the parsed key-value pairs. A variable `last_key` is also initialized to keep track of the last processed key. -3. **Processing Each Line**: - - The function iterates over each line in the split text. - - If a line is empty, it is skipped. - - If a line contains an equals sign (`=`), it is split into a key and a value at the first occurrence of the equals sign. The key is stripped of any leading or trailing whitespace, and the value has any backticks (`) removed. The key-value pair is then added to the dictionary, and `last_key` is updated to the current key. - - If a line does not contain an equals sign but `last_key` is set, the line is considered a continuation of the previous value. The line is appended to the value of `last_key` in the dictionary, with any backticks removed. -4. **Returning the Result**: After processing all lines, the function returns the populated dictionary. - -**Note**: -- The function assumes that each equation is either on a single line or that subsequent lines without an equals sign are continuations of the previous value. -- Backticks (`) in the values are removed during processing. - -**Output Example**: -Given the input text: -``` -x0 = 1 -x1=2 -x2=`2` -x3= def fun():\n print('hello')\n -abc_test1=test -``` -The function would return: -``` -{ - 'x0': '1', - 'x1': '2', - 'x2': '2', - 'x3': "def fun():\nprint('hello')", - 'abc_test1': 'test' -} -``` -## ClassDef MinHeap -**MinHeap**: The function of MinHeap is to implement a minimum heap data structure, which supports efficient retrieval and removal of the smallest element. - -**attributes**: The attributes of this Class. -· heap: A list that stores the elements of the heap. - -**Code Description**: The MinHeap class provides a minimum heap implementation with various methods to manage the heap's elements. The class supports initialization with an optional array, element insertion, element removal, and peeking at the smallest element. It also includes internal methods to maintain the heap property. - -- `__init__(self, arr=None)`: Initializes the heap. If an array is provided, it converts the array into a heap using the `heapify` method. Otherwise, it initializes an empty heap. -- `__contains__(self, item)`: Checks if an item is in the heap using a helper function `contain`. -- `__len__(self)`: Returns the number of elements in the heap. -- `push(self, item)`: Adds a new item to the heap and ensures the heap property is maintained by calling the `_siftup` method. -- `pop(self)`: Removes and returns the smallest item from the heap. It maintains the heap property by calling the `_siftdown` method after removing the root. -- `peek(self)`: Returns the smallest item without removing it from the heap. -- `_siftup(self, idx)`: Ensures the heap property is maintained from a given index upwards to the root. -- `_siftdown(self, idx)`: Ensures the heap property is maintained from a given index downwards to the leaves. -- `heapify(self, arr)`: Converts an array into a heap by copying the array and calling `_siftdown` on each non-leaf node. - -The MinHeap class is utilized in the `backward` method of the `Node` class in `opto\trace\nodes.py`. In this context, MinHeap is used to manage a priority queue for nodes during a backward pass operation. The `backward` method initializes a MinHeap with the current node and uses it to efficiently process nodes in the correct order, ensuring that feedback is propagated correctly through the graph. - -**Note**: -- The elements stored in the heap must support comparison operations (`lt` and `gt` methods). -- The `contain` function used in `__contains__` is assumed to be defined elsewhere in the codebase. - -**Output Example**: -- `push(item)`: Adds `item` to the heap. -- `pop()`: Returns the smallest element, e.g., `3`. -- `peek()`: Returns the smallest element without removing it, e.g., `3`. -- `__len__()`: Returns the number of elements in the heap, e.g., `5`. -- `__contains__(item)`: Returns `True` if `item` is in the heap, otherwise `False`. -### FunctionDef __init__(self, arr) -**__init__**: The function of __init__ is to initialize a MinHeap object, optionally transforming an input array into a valid min-heap. - -**parameters**: The parameters of this Function. -· arr: An optional array to be transformed into a min-heap. If not provided, an empty heap is initialized. - -**Code Description**: The __init__ method is the constructor for the MinHeap class. It initializes the heap based on the provided input array. If no array is provided (`arr` is `None`), it initializes an empty list to represent the heap. If an array is provided, it assigns this array to the heap and then calls the `heapify` method to transform the array into a valid min-heap. - -The `heapify` method is responsible for ensuring that the array satisfies the heap property, where each parent node is less than or equal to its child nodes. This transformation is crucial for the correct functioning of the heap operations. - -**Note**: Points to note about the use of the code -- If an array is provided during initialization, it will be automatically transformed into a min-heap. -- The `heapify` method modifies the heap in place and ensures the heap property is maintained. -- Proper initialization of the heap is essential for the efficiency and correctness of subsequent heap operations such as insertion and deletion. -*** -### FunctionDef __contains__(self, item) -**__contains__**: The function of `__contains__` is to check if a given item is present in the heap of the `MinHeap` class. - -**parameters**: The parameters of this function. -· item: The item to be checked for presence in the heap. - -**Code Description**: The `__contains__` function is a special method in Python that allows the use of the `in` keyword to check for the presence of an item in an instance of the `MinHeap` class. This function takes a single parameter, `item`, which represents the item to be checked. - -Internally, the function calls the `contain` function, passing `self.heap` and `item` as arguments. The `contain` function iterates over the `self.heap` list and checks if the `item` is identical to any of the elements in the list using the `is` operator. If the `item` is found, the `contain` function returns `True`; otherwise, it returns `False`. - -This method provides a convenient way to check for the presence of an item in the heap, leveraging the identity check mechanism provided by the `contain` function. - -**Note**: The `contain` function checks for identity (using the `is` operator) instead of value equality. This means that `__contains__` will only return `True` if the `item` is the exact same object in memory as one of the elements in `self.heap`. - -**Output Example**: -```python -min_heap = MinHeap() -min_heap.heap = [node(1), node(2), node(3)] -item = node(2) -print(item in min_heap) -# Output: True -``` -*** -### FunctionDef __len__(self) -**__len__**: The function of __len__ is to return the number of elements in the MinHeap. - -**parameters**: The parameters of this Function. -· self: Refers to the instance of the MinHeap class. - -**Code Description**: The __len__ method is a special method in Python that is used to define the behavior of the len() function for instances of a class. In this case, the __len__ method is implemented for the MinHeap class. When len() is called on an instance of MinHeap, this method returns the number of elements currently stored in the heap. It achieves this by returning the length of the internal list self.heap, which is used to store the heap elements. - -**Note**: -- This method does not take any parameters other than self. -- It is important to ensure that self.heap is always a list, as the len() function is called on it. - -**Output Example**: -If the MinHeap instance contains 5 elements, calling len(min_heap_instance) will return 5. -*** -### FunctionDef push(self, item) -**push**: The function of push is to add a new item to the MinHeap and maintain the heap property. - -**parameters**: The parameters of this Function. -· item: The item to be added to the heap. - -**Code Description**: The push function is a method of the MinHeap class that adds a new item to the heap and ensures that the heap property is maintained. When an item is pushed onto the heap, it is first appended to the end of the heap list. This operation increases the size of the heap by one. - -After appending the new item, the push function calls the _siftup method with the index of the newly added item, which is the last index of the heap list. The _siftup method is responsible for moving the new item up the heap until the heap property is restored. The heap property in a MinHeap requires that each parent node is less than or equal to its child nodes. The _siftup method ensures that this property is maintained by comparing the new item with its parent and swapping them if necessary. This process continues iteratively until the new item is in a position where the heap property is satisfied or it becomes the root of the heap. - -The push function is used in the backward method of the Node class in the context of a priority queue. In the backward method, nodes are processed in a specific order, and the MinHeap is used to manage this order efficiently. When a parent node needs to be added to the queue, the push function is called to insert the parent node into the MinHeap, ensuring that the heap property is maintained and the nodes are processed in the correct order. - -**Note**: -- The push function relies on the _siftup method to maintain the heap property. -- The heap property ensures that the smallest element is always at the root of the MinHeap. -- The elements in the heap must implement the gt method correctly for the _siftup method to function properly. -- The push function is integral to the operation of the MinHeap in managing the order of nodes in the backward method of the Node class. -*** -### FunctionDef pop(self) -**pop**: The function of pop is to remove and return the root element from the heap. - -**parameters**: -- self: The instance of the MinHeap class. - -**Code Description**: -The pop function is a method of the MinHeap class that is used to remove and return the root element from the heap. The function first checks if the length of the heap is equal to 1, which indicates that there is only one element in the heap. In this case, the function simply calls the pop method of the heap list and returns the popped element. - -If there are more than one element in the heap, the function proceeds to assign the value of the root element (the first element in the heap) to the variable "root". Then, it replaces the root element with the last element in the heap by assigning the popped element from the heap list to the index 0 of the heap list. This step is necessary to maintain the heap property after removing the root element. - -After replacing the root element, the function calls the _siftdown method to sift down the new root element to its correct position in the heap. This ensures that the heap property is maintained and the new root element is correctly positioned relative to its children. - -Finally, the function returns the original root element that was stored in the "root" variable. - -The pop function is called by other methods in the MinHeap class, such as the heapify method. It relies on the _siftdown method to maintain the heap property after removing the root element. - -**Note**: -- The pop function assumes that the heap is represented as a list. -- The function modifies the heap in place and does not return any value. -- Proper use of the pop function is crucial for maintaining the correctness of heap operations and ensuring that the heap property is maintained. - -**Output Example**: -If the heap is [5, 7, 9, 11, 13] and the pop function is called, the function will remove and return the root element, which is 5. After the pop operation, the heap will be [7, 9, 11, 13]. -*** -### FunctionDef peek(self) -**peek**: The function of peek is to return the smallest element in the MinHeap without removing it. - -**parameters**: The parameters of this Function. -· None - -**Code Description**: The peek function is a method of the MinHeap class. It checks if the heap list is non-empty. If the heap contains elements, it returns the first element of the heap list, which is the smallest element due to the properties of a MinHeap. If the heap is empty, it returns None. This function allows users to inspect the smallest element in the heap without modifying the heap structure. - -**Note**: -- The peek function does not alter the state of the heap. -- It is a read-only operation and is useful for checking the minimum element efficiently. - -**Output Example**: -- If the heap is [1, 3, 5, 7], peek() will return 1. -- If the heap is empty, peek() will return None. -*** -### FunctionDef _siftup(self, idx) -**_siftup**: The function of _siftup is to maintain the heap property by moving an element up the heap until the heap property is restored. - -**parameters**: The parameters of this Function. -· idx: The index of the element to be moved up in the heap. - -**Code Description**: The _siftup function is a helper method used to maintain the heap property in a MinHeap data structure. When an element is added to the heap, it may violate the heap property, which requires that each parent node is less than or equal to its child nodes. The _siftup function corrects this by comparing the element at the given index (idx) with its parent. If the element is smaller than its parent, they are swapped. This process continues iteratively until the element is in a position where the heap property is satisfied, or it becomes the root of the heap. - -The function is called by the push method of the MinHeap class. When a new item is added to the heap using the push method, the item is appended to the end of the heap list. The _siftup function is then called with the index of this new item (which is the last index of the list). This ensures that the new item is moved to its correct position in the heap, maintaining the heap property. - -**Note**: -- The _siftup function assumes that the heap property is only violated between the element at the given index and its parent. It does not check or correct violations further up the tree. -- This function is designed to work with a MinHeap, where the smallest element should always be at the root. -- The function relies on the gt method of the elements in the heap to compare their values. Ensure that the elements in the heap implement this method correctly. -*** -### FunctionDef _siftdown(self, idx) -**_siftdown**: The function of _siftdown is to maintain the heap property by sifting down an element at a given index in the heap. - -**parameters**: The parameters of this Function. -· idx: The index of the element to be sifted down in the heap. - -**Code Description**: The _siftdown function is a helper method used to ensure that the heap property is maintained after an element has been moved to a new position in the heap. This function is particularly useful in operations where the heap structure might be violated, such as after removing the root element or during the initial heap construction. - -The function operates as follows: -1. It calculates the index of the last element in the heap. -2. It enters a loop where it calculates the indices of the left and right children of the current element. -3. It initializes the smallest index as the current index. -4. It compares the current element with its left and right children to find the smallest element among them. -5. If one of the children is smaller than the current element, it swaps the current element with the smallest child and updates the current index to the index of the smallest child. -6. The loop continues until the current element is smaller than both of its children or it has no children. - -The function is called by the pop and heapify methods of the MinHeap class: -- In the pop method, _siftdown is used after the root element is removed and the last element is moved to the root position. This ensures that the new root element is correctly positioned to maintain the heap property. -- In the heapify method, _siftdown is called for each non-leaf element in the array to transform the array into a valid heap. - -**Note**: Points to note about the use of the code -- The function assumes that the heap is represented as a list and that each element in the heap has a method lt for comparison. -- The function modifies the heap in place and does not return any value. -- Proper use of _siftdown is crucial for maintaining the efficiency and correctness of heap operations such as insertion, deletion, and heap construction. -*** -### FunctionDef heapify(self, arr) -**heapify**: The function of heapify is to transform an arbitrary array into a valid min-heap. - -**parameters**: The parameters of this Function. -· arr: The array to be transformed into a min-heap. - -**Code Description**: The heapify function is designed to convert a given array into a min-heap, ensuring that the heap property is maintained throughout the array. This function is a method of the MinHeap class and operates as follows: - -1. The function begins by importing the copy module and creating a shallow copy of the input array `arr` to avoid modifying the original array. This copy is stored in the instance variable `self.heap`. -2. It then iterates over the indices of the non-leaf elements of the array in reverse order, starting from the last non-leaf node and moving towards the root. The range for this iteration is calculated as `(len(self.heap) - 2) // 2` to `-1`. -3. For each index `i` in this range, the function calls the helper method `_siftdown(i)`. The _siftdown method is responsible for maintaining the heap property by sifting down the element at index `i` to its correct position in the heap. - -The heapify function is called during the initialization of the MinHeap object if an array is provided. This ensures that any array passed to the MinHeap constructor is automatically transformed into a valid min-heap. - -**Note**: Points to note about the use of the code -- The heapify function assumes that the elements of the array have a method `lt` for comparison, which is used by the _siftdown method. -- The function modifies the heap in place and does not return any value. -- Proper use of the heapify function is crucial for initializing the heap correctly, which in turn ensures the efficiency and correctness of subsequent heap operations such as insertion and deletion. -*** -## FunctionDef for_all_methods(decorator) -**for_all_methods**: The function of for_all_methods is to apply a decorator to all methods of a class. - -**parameters**: -- decorator: The decorator function that will be applied to all methods of the class. - -**Code Description**: -The `for_all_methods` function is a higher-order function that takes a decorator as input and returns a new decorator. The returned decorator can be used to decorate a class, applying the input decorator to all methods of the class. - -The `for_all_methods` function first defines an inner function called `decorate`. This function takes a class as input and iterates over all the attributes of the class using the `__dict__` attribute. For each attribute that is callable (i.e., a method) and does not start with "__" (i.e., not a special method), the function applies the input decorator to the method using the `setattr` function. This effectively replaces the original method with the decorated version. - -Finally, the `decorate` function returns the modified class with the decorated methods. - -The `for_all_methods` function itself returns the `decorate` function, allowing it to be used as a decorator for classes. - -**Note**: -- The input decorator should be a function that takes a method as input and returns a new method. -- The input decorator will be applied to all methods of the class, including inherited methods. -- The input decorator will replace the original methods with the decorated versions. - -**Output Example**: -```python -@for_all_methods -def my_decorator(method): - def wrapper(*args, **kwargs): - # Do something before calling the method - result = method(*args, **kwargs) - # Do something after calling the method - return result - return wrapper - -@my_decorator -class MyClass: - def method1(self): - # Method implementation - - def method2(self): - # Method implementation -``` - -In the above example, the `my_decorator` function is applied to all methods of the `MyClass` class using the `for_all_methods` decorator. The `my_decorator` function wraps each method, allowing additional functionality to be added before and after the method is called. -### FunctionDef decorate(cls) -**decorate**: The function of decorate is to apply a decorator to all callable methods of a class, excluding special methods. - -**parameters**: The parameters of this Function. -· cls: The class whose methods will be decorated. - -**Code Description**: The decorate function iterates over all attributes of the provided class (cls). For each attribute, it checks if the attribute is callable (i.e., a method) and if its name does not start with double underscores (which would indicate a special method). If both conditions are met, the function applies a decorator to the method using the setattr function, which updates the class with the decorated method. Finally, the function returns the modified class. - -**Note**: -- This function assumes that a decorator function named decorator is already defined and available in the scope where decorate is used. -- Special methods (those starting with double underscores) are not decorated by this function. - -**Output Example**: -If you have a class MyClass with methods method1 and method2, after applying the decorate function, both method1 and method2 will be decorated with the decorator function. The class will be returned with these modifications. -*** From 432459a95bff921dca3b50ab18e14ea96e743d15 Mon Sep 17 00:00:00 2001 From: chinganc Date: Wed, 27 Aug 2025 23:33:37 +0000 Subject: [PATCH 167/172] Make train support single-node optimization --- examples/{train_example.py => train_model.py} | 0 examples/train_single_node.py | 20 ++++++++++++++++++ opto/trainer/train.py | 21 +++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) rename examples/{train_example.py => train_model.py} (100%) create mode 100644 examples/train_single_node.py diff --git a/examples/train_example.py b/examples/train_model.py similarity index 100% rename from examples/train_example.py rename to examples/train_model.py diff --git a/examples/train_single_node.py b/examples/train_single_node.py new file mode 100644 index 00000000..13a903fc --- /dev/null +++ b/examples/train_single_node.py @@ -0,0 +1,20 @@ +from opto import trace, trainer + +def main(): + true_number = 3 + train_dataset = dict(inputs=[None], infos=[f'Correct answer is: {true_number}']) + param = trace.node(0, description='An interger to guess', trainable=True) + + trainer.train( + model=param, + # optimizer='OptoPrimeV2', # by default, OPROv2 is used for single-node optimization + train_dataset=train_dataset, + # trainer kwargs + num_epochs=3, + batch_size=1, + verbose='output', + ) + + +if __name__ == "__main__": + main() diff --git a/opto/trainer/train.py b/opto/trainer/train.py index e46d8c4d..e3b79e05 100644 --- a/opto/trainer/train.py +++ b/opto/trainer/train.py @@ -6,6 +6,7 @@ from opto.trainer.guide import Guide from opto.trainer.loggers import BaseLogger from opto.optimizers.optimizer import Optimizer +from opto.trace.nodes import ParameterNode def dataset_check(dataset): @@ -16,11 +17,11 @@ def dataset_check(dataset): def train( *, - model: trace.Module, + model: Union[trace.Module, ParameterNode], train_dataset: dict, # class of optimizer algorithm: Union[Trainer, str] = 'MinibatchAlgorithm', - optimizer: Union[Optimizer, str] = "OptoPrimeV2", + optimizer: Union[Optimizer, str] = None, guide: Union[Guide, str] = 'LLMJudge', logger: Union[BaseLogger, str] = 'ConsoleLogger', # extra configs @@ -42,7 +43,19 @@ def train( # TODO check eligible optimizer, trainer dataset_check(train_dataset) - # TODO remove duplicate codes + if optimizer is None: + optimizer = "OPROv2" if isinstance(model, ParameterNode) else "OptoPrimeV2" + + # Convert ParameterNode to Module + if isinstance(model, ParameterNode): + assert model.trainable, "The parameter must be trainable." + @trace.model + class SingleNodeModel: + def __init__(self, param): + self.param = param # ParameterNode + def forward(self, x): + return self.param + model = SingleNodeModel(model) # Check model parameters is non-empty parameters = model.parameters() @@ -61,7 +74,7 @@ def train( algo = trainer_class( model, optimizer, - logger + logger=logger ) return algo.train( From 851f6215505fcfbdb2ecbf9ba905737e80ea83bb Mon Sep 17 00:00:00 2001 From: windweller Date: Wed, 3 Sep 2025 18:18:08 -0400 Subject: [PATCH 168/172] priority search simple bug fix. async_run accommondates jupyter notebook --- opto/features/priority_search/priority_search.py | 2 +- opto/trainer/utils.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/opto/features/priority_search/priority_search.py b/opto/features/priority_search/priority_search.py index 35342580..91eedffa 100644 --- a/opto/features/priority_search/priority_search.py +++ b/opto/features/priority_search/priority_search.py @@ -408,7 +408,7 @@ def validate(self, candidates, samples, verbose=False, **kwargs): validate_samples.add_samples(samples) # if no validation dataset is provided, append the samples to the validate_samples else: # validate the agents in the validate_dataset # exploration_agents = [rollouts.module for rollouts in samples.samples] # NOTE this might contain some duplicates due to sub_batch_size < batch_size - exploitation_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates + exploration_agents = [c.get_module() for c in exploration_candidates] # get the modules from the exploration candidates exploration_samples = Samples(*self.validate_sampler.sample(exploration_agents, description_prefix='Validating exploration candidates: ')) # sample the exploration agents validate_samples.add_samples(exploration_samples) # append the exploration samples to the validate_samples diff --git a/opto/trainer/utils.py b/opto/trainer/utils.py index ffb6b999..f395a57c 100644 --- a/opto/trainer/utils.py +++ b/opto/trainer/utils.py @@ -33,7 +33,7 @@ def async_run(runs, args_list = None, kwargs_list = None, max_workers = None, de if kwargs_list is None: kwargs_list = [{}] * len(runs) - if (max_workers == 1) and allow_sequential_run: # run without asyncio + if (max_workers == 1) and allow_sequential_run: # run without asyncio print(f"{description} (Running sequentially).") return [run(*args, **kwargs) for run, args, kwargs in zip(runs, args_list, kwargs_list)] else: @@ -41,14 +41,24 @@ async def _run(): loop = asyncio.get_event_loop() with ThreadPoolExecutor(max_workers=max_workers) as executor: tasks = [loop.run_in_executor(executor, functools.partial(run, *args, **kwargs)) - for run, args, kwargs, in zip(runs, args_list, kwargs_list)] + for run, args, kwargs, in zip(runs, args_list, kwargs_list)] # Use the description in the tqdm progress bar if provided if description: return await tqdm_asyncio.gather(*tasks, desc=description) else: return await tqdm_asyncio.gather(*tasks) - return asyncio.run(_run()) + + # Handle Jupyter notebook + try: + return asyncio.run(_run()) + except RuntimeError: + loop = asyncio.get_running_loop() + # We're in a loop (like Jupyter), so we need to run in a new thread + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(asyncio.run, _run()) + return future.result() def batch_run(max_workers=None, description=None): From a3114fb64a41b02963ab964bbdd804e428565991 Mon Sep 17 00:00:00 2001 From: windweller Date: Tue, 9 Sep 2025 17:22:09 -0400 Subject: [PATCH 169/172] fix the node value representation (variable now uses original `repr_node_value`) --- opto/optimizers/optoprime_v2.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 936a086d..4aae5f32 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -495,18 +495,24 @@ def initialize_prompt(self): others_section_title=self.optimizer_prompt_symbol_set.others_section_title.replace(" ", "") ) - @staticmethod - def repr_node_value(node_dict): + def repr_node_value(self, node_dict, node_tag="node", + value_tag="value", constraint_tag="constraint"): temp_list = [] for k, v in node_dict.items(): if "__code" not in k: - constraint_expr = f" ({type(v[0]).__name__}) {k}: {v[1]} " - temp_list.append( - f"\n{v[0]}\n{constraint_expr}\n\n") + if v[1] is not None and node_tag == self.optimizer_prompt_symbol_set.variable_tag: + constraint_expr = f"<{constraint_tag}>\n{v[1]}\n" + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n<{value_tag}>\n{v[0]}\n\n{constraint_expr}\n\n") + else: + temp_list.append( + f"<{node_tag} name=\"{k}\" type=\"{type(v[0]).__name__}\">\n<{value_tag}>\n{v[0]}\n\n\n") else: constraint_expr = f"\n{v[1]}\n" + signature = v[1].replace("The code should start with:\n", "") + func_body = v[0].replace(signature, "") temp_list.append( - f"\n\n{v[0]}\n\n{constraint_expr}\n\n") + f"<{node_tag} name=\"{k}\" type=\"code\">\n<{value_tag}>\n{signature}{func_body}\n\n{constraint_expr}\n\n") return "\n".join(temp_list) def repr_node_value_compact(self, node_dict, node_tag="node", @@ -596,7 +602,7 @@ def problem_instance(self, summary, mask=None): else "" ), variables=( - self.repr_node_value_compact(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, + self.repr_node_value(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, value_tag=self.optimizer_prompt_symbol_set.value_tag, constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.variables_section_title not in mask From 979f49d43e79ebeb86fdae96584b0408f1e2e60a Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 11 Sep 2025 14:15:19 -0400 Subject: [PATCH 170/172] fixing memory representation in optimizers (now XML tag based) --- opto/optimizers/optoprime_v2.py | 73 +++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/opto/optimizers/optoprime_v2.py b/opto/optimizers/optoprime_v2.py index 4aae5f32..cc898bac 100644 --- a/opto/optimizers/optoprime_v2.py +++ b/opto/optimizers/optoprime_v2.py @@ -1,5 +1,5 @@ import json -from typing import Any, List, Dict, Union, Tuple +from typing import Any, List, Dict, Union, Tuple, Optional from dataclasses import dataclass, asdict from opto.optimizers.optoprime import OptoPrime, FunctionFeedback from opto.trace.utils import dedent @@ -92,9 +92,10 @@ def example_output(self, reasoning, variables): else: # Build the output string in the same XML-like format as self.output_format output = [] - output.append(f"<{self.reasoning_tag}>") - output.append(reasoning) - output.append(f"") + if reasoning != "": + output.append(f"<{self.reasoning_tag}>") + output.append(reasoning) + output.append(f"") for var_name, value in variables.items(): output.append(f"<{self.improved_variable_tag}>") output.append(f"<{self.name_tag}>{var_name}") @@ -104,7 +105,6 @@ def example_output(self, reasoning, variables): output.append(f"") return "\n".join(output) - def output_response_extractor(self, response: str) -> Dict[str, Any]: # the response here should just be plain text @@ -143,6 +143,7 @@ def default_prompt_symbols(self) -> Dict[str, str]: "documentation": self.documentation_section_title, } + class OptimizerPromptSymbolSetJSON(OptimizerPromptSymbolSet): """We enforce a JSON output format extraction""" @@ -231,6 +232,7 @@ def output_response_extractor(self, response: str) -> Dict[str, Any]: return extracted_data + class OptimizerPromptSymbolSet2(OptimizerPromptSymbolSet): variables_section_title = "# Variables" inputs_section_title = "# Inputs" @@ -306,11 +308,47 @@ def __repr__(self) -> str: ) +@dataclass +class MemoryInstance: + variables: Dict[str, Tuple[Any, str]] # name -> (data, constraint) + feedback: str + optimizer_prompt_symbol_set: OptimizerPromptSymbolSet + + memory_example_template = dedent( + """{variables}{feedback} + """ + ) + + def __init__(self, variables: Dict[str, Any], feedback: str, optimizer_prompt_symbol_set: OptimizerPromptSymbolSet, + index: Optional[int] = None): + self.feedback = feedback + self.optimizer_prompt_symbol_set = optimizer_prompt_symbol_set + self.variables = variables + self.index = index + + def __str__(self) -> str: + var_repr = "" + for k, v in self.variables.items(): + var_repr += dedent(f""" + <{self.optimizer_prompt_symbol_set.improved_variable_tag}> + <{self.optimizer_prompt_symbol_set.name_tag}>{k} + <{self.optimizer_prompt_symbol_set.value_tag}> + {v[0]} + + + """) + + return self.memory_example_template.format( + variables=var_repr, + feedback=self.feedback, + index=" " + str(self.index) if self.index is not None else "" + ) + + class OptoPrimeV2(OptoPrime): # This is generic representation prompt, which just explains how to read the problem. representation_prompt = dedent( - """ - You're tasked to solve a coding/algorithm problem. You will see the instruction, the code, the documentation of each function used in the code, and the feedback about the execution result. + """You're tasked to solve a coding/algorithm problem. You will see the instruction, the code, the documentation of each function used in the code, and the feedback about the execution result. Specifically, a problem will be composed of the following parts: - {instruction_section_title}: the instruction which describes the things you need to do or the question you should answer. @@ -327,8 +365,7 @@ class OptoPrimeV2(OptoPrime): For variables we express as this: {variable_expression_format} - If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions. - """ + If `data_type` is `code`, it means `{value_tag}` is the source code of a python code, which may include docstring and definitions.""" ) # Optimization @@ -567,16 +604,11 @@ def construct_prompt(self, summary, mask=None, *args, **kwargs): formatted_final = self.final_prompt.format(names=var_names) prefix = user_prompt.split(formatted_final)[0] examples = [] + index = 0 for variables, feedback in self.memory: - examples.append( - json.dumps( - { - "variables": {k: v[0] for k, v in variables.items()}, - "feedback": feedback, - }, - indent=4, - ) - ) + index += 1 + examples.append(str(MemoryInstance(variables, feedback, self.optimizer_prompt_symbol_set, index=index))) + examples = "\n".join(examples) user_prompt = ( prefix @@ -603,8 +635,8 @@ def problem_instance(self, summary, mask=None): ), variables=( self.repr_node_value(summary.variables, node_tag=self.optimizer_prompt_symbol_set.variable_tag, - value_tag=self.optimizer_prompt_symbol_set.value_tag, - constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) + value_tag=self.optimizer_prompt_symbol_set.value_tag, + constraint_tag=self.optimizer_prompt_symbol_set.constraint_tag) if self.optimizer_prompt_symbol_set.variables_section_title not in mask else "" ), @@ -700,7 +732,6 @@ def call_llm( print("LLM response:\n", response) return response - def save(self, path: str): """Save the optimizer state to a file.""" with open(path, 'wb') as f: From 276414a0b5753591c79d66a0e736a96976c52bd5 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 11 Sep 2025 18:03:22 -0400 Subject: [PATCH 171/172] upgrade --- pyproject.toml | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fa4852fe..829af4e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,14 +11,14 @@ authors = [ {name = "Adith Swaminathan", email = "adith387@gmail.com"}, ] license="MIT" -requires-python = ">= 3.9" +requires-python = ">= 3.10" dynamic = ["version", "dependencies", "description"] readme = "README.md" keywords = ["trace", "opto", "AutoDiff"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ] [project.optional-dependencies] diff --git a/setup.py b/setup.py index e1e16725..dbd60be5 100644 --- a/setup.py +++ b/setup.py @@ -29,5 +29,5 @@ long_description=open('README.md', encoding="utf8").read(), packages=setuptools.find_packages(include=["opto*"]), install_requires=install_requires, - python_requires=">=3.9", + python_requires=">=3.10", ) From 5953a5d8e678a45c2b56d8e7b1e2e76e7496eee1 Mon Sep 17 00:00:00 2001 From: windweller Date: Thu, 11 Sep 2025 18:50:44 -0400 Subject: [PATCH 172/172] upgrade workflow --- .github/workflows/ci.yml | 2 +- .github/workflows/python-app.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46e0b317..7889b69d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: # 6) Set up Python & install dependencies - uses: actions/setup-python@v5 - with: { python-version: "3.9" } + with: { python-version: "3.10" } - name: Install Python deps run: | pip install -e . diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index bda57a97..8074be85 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -19,10 +19,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.9 + - name: Set up Python 3.10 uses: actions/setup-python@v3 with: - python-version: "3.9" + python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip
ABCDEFGH
1
2
3
1
2
3
4
5
6