diff --git a/align_system/algorithms/decision_flow_adm/attribute_fine_grained_stage_component.py b/align_system/algorithms/decision_flow_adm/attribute_fine_grained_stage_component.py new file mode 100644 index 00000000..240dad0c --- /dev/null +++ b/align_system/algorithms/decision_flow_adm/attribute_fine_grained_stage_component.py @@ -0,0 +1,191 @@ +import json +from align_system.utils import logging, call_with_coerced_args +from align_system.algorithms.abstracts import ADMComponent +from align_system.utils.alignment_utils import attributes_in_alignment_target +from align_system.data_models.dialog import DialogElement +from align_system.algorithms.decision_flow_adm.utils import validate_structured_response +from align_system.exceptions import SceneSkipException + +log = logging.getLogger(__name__) + + +class AttributeFineGrainedStageComponent(ADMComponent): + """ + Fine-grained attribute stage component that uses explicit numeric target values + instead of binary high/low distinctions. + + This component extracts attributes using the Phase2FineGrainedAttributePrompt which + provides scale anchor examples at specific value levels (0.0-1.0) for more + precise value targeting. + """ + + def __init__( + self, + structured_inference_engine, + scenario_description_template, + system_prompt_template, + prompt_template, + output_schema_template, + max_json_retries=5, + attributes=None, + **kwargs, + ): + self.structured_inference_engine = structured_inference_engine + self.scenario_description_template = scenario_description_template + self.system_prompt_template = system_prompt_template + self.prompt_template = prompt_template + self.output_schema_template = output_schema_template + self.max_json_retries = max_json_retries + + if attributes is None: + attributes = {} + self.attributes = attributes + + def run_returns(self): + return "attribute_analysis" + + def run(self, scenario_state, choices, extraction=None, variables=None, alignment_target=None, **kwargs): + """ + Identify and analyze key attributes relevant to decision making using + fine-grained value targeting. + + Unlike the high/low attribute stage, this component uses explicit numeric + values (0.0-1.0) from the alignment_target to guide attribute extraction + at specific scale points. + """ + + # Handle alignment_target workflow similar to comparative_regression_adm_component + if alignment_target is None: + # No alignment target - use all attributes + target_attributes = list(self.attributes.values()) + else: + # Alignment target provided - ONLY use attributes in the alignment target + target_attribute_names = attributes_in_alignment_target(alignment_target) + target_attributes = [self.attributes[n] for n in target_attribute_names if n in self.attributes] + + attribute_results = {} + + # Build a dictionary mapping KDMA names to their values from alignment_target + kdma_value_dict = {} + if alignment_target is not None and hasattr(alignment_target, 'kdma_values'): + for kdma_entry in alignment_target.kdma_values: + # Support both AlignmentTarget API (KDMAValue objects) and dict access + if isinstance(kdma_entry, dict): + kdma_name = kdma_entry.get('kdma') + kdma_value = kdma_entry.get('value') + else: + # KDMAValue object with property accessors (API-compliant) + kdma_name = kdma_entry.kdma + kdma_value = kdma_entry.value + + # Only store if both name and value are present + if kdma_name is not None and kdma_value is not None: + kdma_value_dict[kdma_name] = kdma_value + + # Process each attribute individually, similar to comparative_regression_adm_component + for attribute in target_attributes: + scenario_description = call_with_coerced_args( + self.scenario_description_template, + { + 'scenario_state': scenario_state, + 'alignment_target': alignment_target, + 'attribute': attribute.name, + 'attributes_of_interest': {attribute.name} + }) + + dialog = [] + if self.system_prompt_template is not None: + system_prompt = call_with_coerced_args( + self.system_prompt_template, + {'target_attribute': attribute} + ) + + dialog.insert(0, DialogElement(role='system', + content=system_prompt)) + + log.info(f"Processing attribute (fine-grained): {attribute.name}") + log.info(f"Scenario description: {scenario_description}") + + # Get the numeric value for this attribute from the alignment target + # Use attribute.kdma (not attribute.name) to match alignment_target kdma_values + attribute_value = kdma_value_dict.get(attribute.kdma) + + # Use the actual numeric value (0.0-1.0) for fine-grained targeting + # Default to 0.5 (moderate) if no value is available + # if attribute_value is None: + # log.warning(f"No value found for attribute {attribute.name} (kdma: {attribute.kdma}), defaulting to 0.5") + # attribute_value = 0.5 + + log.info(f"Target value for {attribute.name}: {attribute_value}") + + # Call prompt template with target_attribute and target_value + # instead of target_bias (which was "high/low attribute_name") + prompt = call_with_coerced_args( + self.prompt_template, + { + 'scenario_description': scenario_description, + 'choices': choices, + 'extraction': extraction, + 'variables': variables, + 'target_attribute': attribute.name, + 'target_value': attribute_value + }, + ) + log.info(f"Fine-grained attribute prompt for {attribute.name} (value={attribute_value})") + + dialog.append(DialogElement(role='user', + content=prompt)) + + output_schema = call_with_coerced_args( + self.output_schema_template, + {}) + + dialog_prompt = self.structured_inference_engine.dialog_to_prompt(dialog) + + log.info("=" * 80) + log.info(f"Attribute Stage Dialog Prompt ({attribute.name})") + log.info("=" * 80) + log.info(dialog_prompt) + log.info("=" * 80) + + # Retry loop for structured inference with validation + response = None + last_error = None + context_str = f" for {attribute.name} (value={attribute_value})" + + for attempt in range(self.max_json_retries): + try: + # Run structured inference + raw_response = self.structured_inference_engine.run_inference( + dialog_prompt, + output_schema + ) + + # Validate response + response = validate_structured_response(raw_response) + + # Success - break out of retry loop + log.info(f"Fine-grained attribute stage inference succeeded on attempt {attempt + 1}{context_str}") + break + + except (json.JSONDecodeError, ValueError, TypeError) as e: + last_error = e + log.warning( + f"Fine-grained attribute stage JSON decode error on attempt {attempt + 1}/{self.max_json_retries}{context_str}: {e}" + ) + + if attempt < self.max_json_retries - 1: + log.info(f"Retrying fine-grained attribute stage inference{context_str}...") + else: + log.error(f"Fine-grained attribute stage failed after {self.max_json_retries} attempts{context_str}") + raise SceneSkipException( + f"Failed to generate valid JSON after {self.max_json_retries} attempts{context_str}. " + f"Last error: {last_error}", + component_name="AttributeFineGrainedStageComponent", + last_error=last_error + ) from last_error + + log.info(f"Fine-grained attribute analysis for {attribute.name} (value={attribute_value}) completed: {response.get('Variable', [])}") + attribute_results[attribute.name] = response.get('Variable', []) + + return attribute_results diff --git a/align_system/algorithms/decision_flow_adm/express_stage_unstructured.py b/align_system/algorithms/decision_flow_adm/express_stage_unstructured.py index 149163dd..b5b942d2 100644 --- a/align_system/algorithms/decision_flow_adm/express_stage_unstructured.py +++ b/align_system/algorithms/decision_flow_adm/express_stage_unstructured.py @@ -23,23 +23,39 @@ class ExpressStageUnstructuredComponent(ADMComponent): def __init__( self, structured_inference_engine, - scenario_description_template, system_prompt_template, prompt_template, max_json_retries=5, + attributes=None, **kwargs, ): self.structured_inference_engine = structured_inference_engine - self.scenario_description_template = scenario_description_template self.system_prompt_template = system_prompt_template self.prompt_template = prompt_template self.max_json_retries = max_json_retries + if attributes is None: + attributes = {} + self.attributes = attributes + def run_returns(self): return "mathematical_model" - def run(self, scenario_state, choices, objective_function=None, filter_analysis=None, attribute_analysis=None, variables=None, extraction=None, alignment_target=None, **kwargs): - """Create complete mathematical optimization model following math_express template""" + def run(self, objective_function=None, variables=None, extraction=None, alignment_target=None, **kwargs): + """Create complete mathematical optimization model following math_express template. + + Args: + objective_function: Output from Objective stage containing: + - objective_function: The formula string + - filtered_pairs: List of Variable-Attribute-Value triplets that passed weight threshold + variables: List of variables from Variables stage + extraction: List of extracted information from Extraction stage + alignment_target: AlignmentTarget with kdma_values for target attribute context + **kwargs: Additional pipeline arguments (ignored) + + Note: Uses filtered_pairs from Objective stage (already filtered by weight > 0.3). + Matches original DecisionFlow math_express which only takes structure. + """ # Build structure following decision_flow_stages.py lines 158-188 structure = {} @@ -53,30 +69,21 @@ def run(self, scenario_state, choices, objective_function=None, filter_analysis= else: structure["objective_function"] = objective_function if objective_function else 'weight * attribute of variable' - # 3. Attributes from filtered analysis (lines 162-175) + # 3. Attributes from Objective stage's filtered pairs (already filtered by weight threshold) + # This matches original DecisionFlow which uses filtered triplets structure["attribute"] = [] - if filter_analysis and attribute_analysis: - for attribute_name, filter_data in filter_analysis.items(): - # Skip environment attributes (line 164) - if attribute_name.lower() == "environment": + if objective_function and isinstance(objective_function, dict): + filtered_pairs = objective_function.get('filtered_pairs', []) + for pair in filtered_pairs: + # Skip environment attributes (check if 'environment' is in the variable name) + variable = pair.get('Variable', '') + if 'environment' in variable.lower(): continue - - # Get attribute analysis data for this attribute - attribute_data = attribute_analysis.get(attribute_name, []) - - # Process attribute data to create triples (variable, attribute, value) - if isinstance(attribute_data, list): - for variable_info in attribute_data: - if isinstance(variable_info, dict) and 'Variable' in variable_info: - variable_name = variable_info['Variable'] - if 'Attribute' in variable_info: - for attr_info in variable_info['Attribute']: - if isinstance(attr_info, dict) and 'Attribute' in attr_info and 'Value' in attr_info: - structure["attribute"].append({ - "Variable": variable_name, - "Attribute": attr_info['Attribute'], - "Value": attr_info['Value'] - }) + structure["attribute"].append({ + "Variable": variable, + "Attribute": pair.get('Attribute', ''), + "Value": pair.get('Value', []) + }) # 4. Constraints from extraction information (lines 177-188) structure["constraints"] = [] @@ -89,12 +96,36 @@ def run(self, scenario_state, choices, objective_function=None, filter_analysis= if any(indicator in info_item.lower() for indicator in constraint_indicators): structure["constraints"].append(info_item) - scenario_description = call_with_coerced_args( - self.scenario_description_template, - { - 'scenario_state': scenario_state, - 'alignment_target': alignment_target - }) + # 5. Target Attribute with positive/negative alignment (matches original DecisionFlow) + # Read descriptions from attribute YAML configs + structure["target_attribute"] = {} + if alignment_target is not None and hasattr(alignment_target, 'kdma_values'): + for kdma_entry in alignment_target.kdma_values: + # Support both KDMAValue objects and dict access + if isinstance(kdma_entry, dict): + kdma_name = kdma_entry.get('kdma') + kdma_value = kdma_entry.get('value') + else: + kdma_name = kdma_entry.kdma + kdma_value = kdma_entry.value + + if kdma_name and kdma_name in self.attributes: + attr = self.attributes[kdma_name] + description = getattr(attr, 'description', '') or '' + score_examples = getattr(attr, 'score_examples', '') or '' + attr_name = getattr(attr, 'name', kdma_name) + + # Determine high/low focus based on target value + focus = "high" if (kdma_value is not None and kdma_value >= 0.5) else "low" + + # Build positive/negative alignment from description and score_examples + structure["target_attribute"][kdma_name] = { + "focus": focus, + "description": description, + "positive_alignment": f"Actions demonstrating {focus} {attr_name}", + "negative_alignment": f"Actions demonstrating {'low' if focus == 'high' else 'high'} {attr_name}", + "score_examples": score_examples + } dialog = [] if self.system_prompt_template is not None: @@ -106,13 +137,10 @@ def run(self, scenario_state, choices, objective_function=None, filter_analysis= dialog.insert(0, DialogElement(role='system', content=system_prompt)) + # Express stage only requires structure (matches original DecisionFlow math_express) prompt = call_with_coerced_args( self.prompt_template, - { - 'scenario_description': scenario_description, - 'choices': choices, - 'structure': structure - }, + {'structure': structure}, ) dialog.append(DialogElement(role='user', diff --git a/align_system/algorithms/decision_flow_adm/filter_stage_component.py b/align_system/algorithms/decision_flow_adm/filter_stage_component.py index ab626a60..a72c9eae 100644 --- a/align_system/algorithms/decision_flow_adm/filter_stage_component.py +++ b/align_system/algorithms/decision_flow_adm/filter_stage_component.py @@ -36,7 +36,12 @@ def run_returns(self): return "filter_analysis" def run(self, scenario_state, choices, attribute_analysis=None, variables=None, extraction=None, alignment_target=None, **kwargs): - """Filter and weight attributes based on their relevance to the target bias""" + """Filter and weight attributes based on relevance to target bias. + + Following DecisionFlow reference: each Attribute is evaluated individually + against the target bias. The weight is then applied to all Variable-Attribute + pairs containing that attribute. + """ # Handle alignment_target workflow similar to attribute_stage_component if alignment_target is None: @@ -47,96 +52,138 @@ def run(self, scenario_state, choices, attribute_analysis=None, variables=None, target_attribute_names = attributes_in_alignment_target(alignment_target) target_attributes = [self.attributes[n] for n in target_attribute_names if n in self.attributes] + # filter_results will contain weighted Variable-Attribute pairs organized by KDMA filter_results = {} - # Process each attribute's analysis individually to determine relevance and weight - for attribute in target_attributes: - # Get the attribute analysis results for this specific attribute - attribute_data = attribute_analysis.get(attribute.name, []) if attribute_analysis else [] - - scenario_description = call_with_coerced_args( - self.scenario_description_template, - { - 'scenario_state': scenario_state, - 'alignment_target': alignment_target, - 'attribute': attribute.name, - 'attributes_of_interest': {attribute.name} - }) - - dialog = [] - if self.system_prompt_template is not None: - system_prompt = call_with_coerced_args( - self.system_prompt_template, - {'target_attribute': attribute} - ) - - dialog.insert(0, DialogElement(role='system', - content=system_prompt)) - - log.info(f"Filtering attribute: {attribute.name}") - log.info(f"Scenario description: {scenario_description}") - - prompt = call_with_coerced_args( - self.prompt_template, - { - 'scenario_description': scenario_description, - 'choices': choices, - 'attribute_information': attribute_data, - 'target_bias': attribute - }, - ) - log.info(f"Filter prompt: {prompt}") - - dialog.append(DialogElement(role='user', - content=prompt)) - - output_schema = call_with_coerced_args( - self.output_schema_template, - {}) - - dialog_prompt = self.structured_inference_engine.dialog_to_prompt(dialog) - - # Retry loop for structured inference with validation - response = None - last_error = None - context_str = f" for {attribute.name}" - - for attempt in range(self.max_json_retries): - try: - # Run structured inference - raw_response = self.structured_inference_engine.run_inference( - dialog_prompt, - output_schema - ) - - # Validate response - response = validate_structured_response(raw_response) - - # Success - break out of retry loop - log.info(f"Filter stage inference succeeded on attempt {attempt + 1}{context_str}") - break - - except (json.JSONDecodeError, ValueError, TypeError) as e: - last_error = e - log.warning( - f"Filter stage JSON decode error on attempt {attempt + 1}/{self.max_json_retries}{context_str}: {e}" + # Process each KDMA's attribute analysis + for target_attr in target_attributes: + kdma_name = target_attr.name + # Get the attribute analysis results for this KDMA + attribute_data = attribute_analysis.get(kdma_name, []) if attribute_analysis else [] + + if not attribute_data: + log.info(f"No attribute data for {kdma_name}, skipping") + continue + + log.info("=" * 60) + log.info(f"Filter Stage: Processing KDMA '{kdma_name}'") + log.info("=" * 60) + + # Collect unique attribute names across all variables + unique_attributes = set() + for var_entry in attribute_data: + if not isinstance(var_entry, dict): + continue + if var_entry.get("Variable") == "Environment": + continue + for attr_entry in var_entry.get("Attribute", []): + if isinstance(attr_entry, dict) and attr_entry.get("Attribute"): + unique_attributes.add(attr_entry.get("Attribute")) + + log.info(f"Found {len(unique_attributes)} unique attributes to evaluate: {unique_attributes}") + + # Evaluate each unique attribute against target bias (cache weights) + attribute_weights = {} + for attr_name in unique_attributes: + log.info(f"Filtering attribute: {attr_name}") + + dialog = [] + if self.system_prompt_template is not None: + system_prompt = call_with_coerced_args( + self.system_prompt_template, + {'target_attribute': target_attr} ) + dialog.insert(0, DialogElement(role='system', content=system_prompt)) + + prompt = call_with_coerced_args( + self.prompt_template, + { + 'attribute_name': attr_name, + 'target_bias': target_attr + }, + ) - if attempt < self.max_json_retries - 1: - log.info(f"Retrying Filter stage inference{context_str}...") - else: - log.error(f"Filter stage failed after {self.max_json_retries} attempts{context_str}") - raise SceneSkipException( - f"Failed to generate valid JSON after {self.max_json_retries} attempts{context_str}. " - f"Last error: {last_error}", - component_name="FilterStageComponent", - last_error=last_error - ) from last_error - - log.info(f"Filter analysis for {attribute.name} completed: Weight={response.get('Weight', 0)}") - filter_results[attribute.name] = { - 'explanation': response.get('Explanation', ''), - 'weight': response.get('Weight', 0) + dialog.append(DialogElement(role='user', content=prompt)) + + output_schema = call_with_coerced_args( + self.output_schema_template, + {}) + + dialog_prompt = self.structured_inference_engine.dialog_to_prompt(dialog) + + # Retry loop for structured inference with validation + response = None + last_error = None + + for attempt in range(self.max_json_retries): + try: + raw_response = self.structured_inference_engine.run_inference( + dialog_prompt, + output_schema + ) + response = validate_structured_response(raw_response) + log.info(f"Filter inference succeeded for '{attr_name}' on attempt {attempt + 1}") + break + + except (json.JSONDecodeError, ValueError, TypeError) as e: + last_error = e + log.warning( + f"Filter JSON decode error for '{attr_name}' on attempt {attempt + 1}/{self.max_json_retries}: {e}" + ) + if attempt < self.max_json_retries - 1: + log.info(f"Retrying filter inference for '{attr_name}'...") + else: + log.error(f"Filter failed for '{attr_name}' after {self.max_json_retries} attempts") + raise SceneSkipException( + f"Failed to generate valid JSON after {self.max_json_retries} attempts for '{attr_name}'. " + f"Last error: {last_error}", + component_name="FilterStageComponent", + last_error=last_error + ) from last_error + + weight = response.get('Weight', 0) + explanation = response.get('Explanation', '') + + log.info(f" -> Weight: {weight}, Explanation: {explanation[:100]}...") + + # Cache the weight for this attribute + attribute_weights[attr_name] = { + 'Weight': weight, + 'Explanation': explanation + } + + # Apply cached weights to all Variable-Attribute pairs + weighted_pairs = [] + for var_entry in attribute_data: + if not isinstance(var_entry, dict): + continue + + variable_name = var_entry.get("Variable", "") + if not variable_name or variable_name == "Environment": + continue + + for attr_entry in var_entry.get("Attribute", []): + if not isinstance(attr_entry, dict): + continue + + attr_name = attr_entry.get("Attribute", "") + attr_values = attr_entry.get("Value", []) + + if attr_name and attr_name in attribute_weights: + weighted_pairs.append({ + "Variable": variable_name, + "Attribute": attr_name, + "Value": attr_values, + "Weight": attribute_weights[attr_name]['Weight'], + "Explanation": attribute_weights[attr_name]['Explanation'] + }) + + # Store all weighted pairs for this KDMA + filter_results[kdma_name] = { + 'weighted_pairs': weighted_pairs, + 'target_bias': target_attr.name } + log.info(f"Filter analysis for '{kdma_name}' completed: {len(unique_attributes)} attributes evaluated, {len(weighted_pairs)} pairs created") + return filter_results diff --git a/align_system/algorithms/decision_flow_adm/math_reason_fine_grained_stage_component.py b/align_system/algorithms/decision_flow_adm/math_reason_fine_grained_stage_component.py new file mode 100644 index 00000000..d51702d9 --- /dev/null +++ b/align_system/algorithms/decision_flow_adm/math_reason_fine_grained_stage_component.py @@ -0,0 +1,325 @@ +import json +from align_system.utils import logging, call_with_coerced_args +from align_system.algorithms.abstracts import ADMComponent +from align_system.utils.alignment_utils import attributes_in_alignment_target +from align_system.data_models.dialog import DialogElement +from align_system.algorithms.decision_flow_adm.utils import validate_structured_response +from align_system.exceptions import SceneSkipException + +log = logging.getLogger(__name__) + + +class MathReasonFineGrainedStageComponent(ADMComponent): + """ + Fine-grained MathReason stage component using numeric target values (0.0-1.0) + instead of binary high/low bias. + + This component provides more precise alignment by passing specific numeric + targets for each KDMA (e.g., affiliation: 0.9, merit: 0.3) to guide the + mathematical reasoning process. + """ + + def __init__( + self, + structured_inference_engine, + scenario_description_template, + system_prompt_template, + prompt_template, + output_schema_template, + max_json_retries=5, + attributes=None, + **kwargs, + ): + self.structured_inference_engine = structured_inference_engine + self.scenario_description_template = scenario_description_template + self.system_prompt_template = system_prompt_template + self.prompt_template = prompt_template + self.output_schema_template = output_schema_template + self.max_json_retries = max_json_retries + + if attributes is None: + attributes = {} + self.attributes = attributes + + def run_returns(self): + return ('chosen_choice', 'justification') + + def run(self, scenario_state, choices, actions, mathematical_model=None, attribute_analysis=None, objective_function=None, alignment_target=None, **kwargs): + """Use fine-grained math_reason prompt with numeric target values""" + + log.info("=" * 80) + log.info("MathReasonFineGrained Stage Starting") + log.info("=" * 80) + + # Handle alignment_target workflow similar to other stage components + if alignment_target is None: + # No alignment target - use all attributes + target_attributes = list(self.attributes.values()) + target_attribute_names = [] + else: + # Alignment target provided - ONLY use attributes in the alignment target + target_attribute_names = attributes_in_alignment_target(alignment_target) + target_attributes = [self.attributes[n] for n in target_attribute_names if n in self.attributes] + + try: + # Format structure for math_reason prompt + if mathematical_model and isinstance(mathematical_model, dict): + # Check if we have the nested 'mathematical_model' key (from Express stage output) + if 'mathematical_model' in mathematical_model: + nested_model = mathematical_model['mathematical_model'] + structure = { + "Objective Function": nested_model.get('Objective Function', 'weight * attribute of variable'), + "Decision Variables": nested_model.get('Decision Variables', []), + "Constraints": nested_model.get('Constraints', []) + } + else: + # Direct access (for backwards compatibility) + structure = { + "Objective Function": mathematical_model.get('Objective Function', 'weight * attribute of variable'), + "Decision Variables": mathematical_model.get('Decision Variables', []), + "Constraints": mathematical_model.get('Constraints', []) + } + else: + # Fallback structure + structure = { + "Objective Function": "weight * attribute of variable", + "Decision Variables": [], + "Constraints": [] + } + + # Format attribute data for math_reason prompt using filtered_pairs from objective stage + attribute = [] + + # Extract filtered_pairs from objective_function output (preferred - only pairs with weight > threshold) + filtered_pairs = [] + if objective_function and isinstance(objective_function, dict): + filtered_pairs = objective_function.get('filtered_pairs', []) + + if filtered_pairs: + # Use filtered pairs (Variable-Attribute pairs that exceeded weight threshold) + log.info(f"Using {len(filtered_pairs)} filtered pairs from Objective stage") + for pair in filtered_pairs: + attribute.append({ + "Variable": pair.get("Variable", ""), + "Attribute": pair.get("Attribute", ""), + "Value": pair.get("Value", []) + }) + log.info(f" - {pair.get('Variable', '')[:50]}... | {pair.get('Attribute', '')} | Weight: {pair.get('Weight', 'N/A')}") + elif attribute_analysis: + # Fallback to attribute_analysis if no filtered_pairs available + log.info("No filtered_pairs available, falling back to attribute_analysis") + for attr_name, variables_data in attribute_analysis.items(): + if isinstance(variables_data, list): + for i, var_data in enumerate(variables_data): + if isinstance(var_data, dict): + variable_name = var_data.get("Variable", "") + + # Check if 'Attribute' key contains nested list of attribute dicts + if 'Attribute' in var_data and isinstance(var_data['Attribute'], list): + # Iterate through nested attributes and extract all values + all_values = [] + for nested_attr in var_data['Attribute']: + if isinstance(nested_attr, dict) and 'Value' in nested_attr: + nested_values = nested_attr.get('Value', []) + if isinstance(nested_values, list): + all_values.extend(nested_values) + else: + all_values.append(nested_values) + + attribute.append({ + "Variable": variable_name, + "Attribute": attr_name, + "Value": all_values + }) + else: + # Fallback to old behavior for backwards compatibility + attribute.append({ + "Variable": variable_name, + "Attribute": attr_name, + "Value": var_data.get("Value", "") + }) + + # Format choices as the math_reason prompt expects + formatted_choices = [f"({i}) {choice}" for i, choice in enumerate(choices)] + + # Build KDMA value dictionary for fine-grained target values (API-compliant) + kdma_value_dict = {} + if alignment_target is not None and hasattr(alignment_target, 'kdma_values'): + for kdma_entry in alignment_target.kdma_values: + # Support both AlignmentTarget API (KDMAValue objects) and dict access + if isinstance(kdma_entry, dict): + kdma_name = kdma_entry.get('kdma') + kdma_value = kdma_entry.get('value') + else: + # KDMAValue object with property accessors (API-compliant) + kdma_name = kdma_entry.kdma + kdma_value = kdma_entry.value + + # Only store if both name and value are present + if kdma_name is not None and kdma_value is not None: + kdma_value_dict[kdma_name] = kdma_value + + # Generate fine-grained target attributes with numeric values AND descriptions + # The description helps the LLM understand what each attribute means for scoring + if alignment_target and target_attributes: + target_attrs_values = {} + for target_attr in target_attributes: + # Use target_attr.kdma to match alignment_target kdma_values + attr_value = kdma_value_dict.get(target_attr.kdma) + if attr_value is not None: + # Include both value and description (like original DecisionFlow's target_bias) + target_attrs_values[target_attr.name] = { + "value": attr_value, + "description": getattr(target_attr, 'description', None) + } + # Note: Unlike high/low version, we don't use defaults - if no value, we don't include it + + # Format as a readable string for the prompt + target_attributes_values_str = json.dumps(target_attrs_values, indent=2) + else: + target_attributes_values_str = "{}" + + dialog = [] + if self.system_prompt_template is not None: + system_prompt = call_with_coerced_args( + self.system_prompt_template, + { + 'alignment_target': alignment_target, + 'target_attributes': target_attributes + } + ) + dialog.insert(0, DialogElement(role='system', + content=system_prompt)) + + prompt = call_with_coerced_args( + self.prompt_template, + { + 'objective': structure["Objective Function"], + 'attribute': attribute, + 'variable': structure["Decision Variables"], + 'constraints': structure["Constraints"], + 'choice': formatted_choices, + 'target_attributes_values': target_attributes_values_str + }, + ) + + dialog.append(DialogElement(role='user', + content=prompt)) + + output_schema = call_with_coerced_args( + self.output_schema_template, + {}) + + dialog_prompt = self.structured_inference_engine.dialog_to_prompt(dialog) + + log.info("=" * 80) + log.info("MathReasonFineGrained Dialog Prompt") + log.info("=" * 80) + log.info(dialog_prompt) + log.info("=" * 80) + + # Retry loop for structured inference with validation + response = None + last_error = None + + import time + inference_start = time.time() + + for attempt in range(self.max_json_retries): + try: + # Run structured inference + log.info(f"Running inference attempt {attempt + 1}/{self.max_json_retries}...") + raw_response = self.structured_inference_engine.run_inference( + dialog_prompt, + output_schema + ) + + log.info("=" * 80) + log.info("MathReasonFineGrained Raw Response") + log.info("=" * 80) + log.info(f"{raw_response}") + log.info("=" * 80) + + # Validate response + response = validate_structured_response(raw_response) + + # Success - break out of retry loop + inference_elapsed = time.time() - inference_start + log.info(f"MathReasonFineGrained stage inference succeeded on attempt {attempt + 1} (took {inference_elapsed:.2f}s)") + break + + except (json.JSONDecodeError, ValueError, TypeError) as e: + last_error = e + log.warning( + f"MathReasonFineGrained stage JSON decode error on attempt {attempt + 1}/{self.max_json_retries}: {e}" + ) + + if attempt < self.max_json_retries - 1: + log.info("Retrying MathReasonFineGrained stage inference...") + else: + log.error(f"MathReasonFineGrained stage failed after {self.max_json_retries} attempts") + raise SceneSkipException( + f"Failed to generate valid JSON after {self.max_json_retries} attempts. " + f"Last error: {last_error}", + component_name="MathReasonFineGrainedStageComponent", + last_error=last_error + ) from last_error + + # Parse response to get chosen choice (string-based selection) + reasoning = response.get('Reasoning', '') + answer_text = response.get('Answer', '') + + log.info("=" * 80) + log.info("MathReasonFineGrained Parsed Response") + log.info("=" * 80) + log.info(f"Answer Text: {answer_text}") + log.info(f"Reasoning: {reasoning}") + log.info("=" * 80) + + # Find matching choice using fuzzy matching + chosen_choice = None + + # Try exact match first + if answer_text in choices: + chosen_choice = answer_text + log.info(f"Exact match found for answer: {answer_text}") + else: + # Try substring match (answer contains choice or vice versa) + for choice in choices: + if answer_text.strip() in choice or choice in answer_text.strip(): + chosen_choice = choice + log.info(f"Substring match: '{answer_text}' matched to '{choice}'") + break + + # Try case-insensitive match + if chosen_choice is None: + answer_lower = answer_text.lower().strip() + for choice in choices: + if answer_lower in choice.lower() or choice.lower() in answer_lower: + chosen_choice = choice + log.info(f"Case-insensitive match: '{answer_text}' matched to '{choice}'") + break + + # Fallback to first choice if no match found + if chosen_choice is None: + log.warning(f"Could not match answer '{answer_text}' to any choice, defaulting to first choice") + chosen_choice = choices[0] + + log.info("=" * 80) + log.info("MathReasonFineGrained Final Selection") + log.info("=" * 80) + log.info(f"Selected choice: {chosen_choice}") + log.info(f"Justification: {reasoning[:200]}{'...' if len(reasoning) > 200 else ''}") + log.info("=" * 80) + log.info("MathReasonFineGrained Stage Completed Successfully") + log.info("=" * 80) + + # Return chosen_choice and justification - EnsureChosenActionADMComponent will convert to action + return chosen_choice, reasoning + + except Exception as e: + log.warning(f"MathReasonFineGrainedStageComponent failed with error: {e}") + log.warning("Falling back to first choice") + + # Fallback: return first choice string and error justification + return choices[0], f"Math reasoning failed, selected first choice. Error: {str(e)}" diff --git a/align_system/algorithms/decision_flow_adm/math_reason_stage_component.py b/align_system/algorithms/decision_flow_adm/math_reason_stage_component.py index 8f0f7038..7283c82f 100644 --- a/align_system/algorithms/decision_flow_adm/math_reason_stage_component.py +++ b/align_system/algorithms/decision_flow_adm/math_reason_stage_component.py @@ -35,9 +35,13 @@ def __init__( def run_returns(self): return 'chosen_action' - def run(self, scenario_state, choices, actions, mathematical_model=None, attribute_analysis=None, alignment_target=None, **kwargs): + def run(self, scenario_state, choices, actions, mathematical_model=None, attribute_analysis=None, objective_function=None, alignment_target=None, **kwargs): """Use math_reason prompt to select optimal action based on mathematical model""" + log.info("=" * 80) + log.info("MathReason Stage Starting") + log.info("=" * 80) + # Handle alignment_target workflow similar to other stage components if alignment_target is None: # No alignment target - use all attributes @@ -51,11 +55,21 @@ def run(self, scenario_state, choices, actions, mathematical_model=None, attribu try: # Format structure for math_reason prompt if mathematical_model and isinstance(mathematical_model, dict): - structure = { - "Objective Function": mathematical_model.get('Objective Function', 'weight * attribute of variable'), - "Decision Variables": mathematical_model.get('Decision Variables', []), - "Constraints": mathematical_model.get('Constraints', []) - } + # Check if we have the nested 'mathematical_model' key (from Express stage output) + if 'mathematical_model' in mathematical_model: + nested_model = mathematical_model['mathematical_model'] + structure = { + "Objective Function": nested_model.get('Objective Function', 'weight * attribute of variable'), + "Decision Variables": nested_model.get('Decision Variables', []), + "Constraints": nested_model.get('Constraints', []) + } + else: + # Direct access (for backwards compatibility) + structure = { + "Objective Function": mathematical_model.get('Objective Function', 'weight * attribute of variable'), + "Decision Variables": mathematical_model.get('Decision Variables', []), + "Constraints": mathematical_model.get('Constraints', []) + } else: # Fallback structure structure = { @@ -64,18 +78,57 @@ def run(self, scenario_state, choices, actions, mathematical_model=None, attribu "Constraints": [] } - # Format attribute data for math_reason prompt + # Format attribute data for math_reason prompt using filtered_pairs from objective stage attribute = [] - if attribute_analysis: + + # Extract filtered_pairs from objective_function output (preferred - only pairs with weight > threshold) + filtered_pairs = [] + if objective_function and isinstance(objective_function, dict): + filtered_pairs = objective_function.get('filtered_pairs', []) + + if filtered_pairs: + # Use filtered pairs (Variable-Attribute pairs that exceeded weight threshold) + log.info(f"Using {len(filtered_pairs)} filtered pairs from Objective stage") + for pair in filtered_pairs: + attribute.append({ + "Variable": pair.get("Variable", ""), + "Attribute": pair.get("Attribute", ""), + "Value": pair.get("Value", []) + }) + log.info(f" - {pair.get('Variable', '')[:50]}... | {pair.get('Attribute', '')} | Weight: {pair.get('Weight', 'N/A')}") + elif attribute_analysis: + # Fallback to attribute_analysis if no filtered_pairs available + log.info("No filtered_pairs available, falling back to attribute_analysis") for attr_name, variables_data in attribute_analysis.items(): if isinstance(variables_data, list): - for var_data in variables_data: + for i, var_data in enumerate(variables_data): if isinstance(var_data, dict): - attribute.append({ - "Variable": var_data.get("Variable", ""), - "Attribute": attr_name, - "Value": var_data.get("Value", "") - }) + variable_name = var_data.get("Variable", "") + + # Check if 'Attribute' key contains nested list of attribute dicts + if 'Attribute' in var_data and isinstance(var_data['Attribute'], list): + # Iterate through nested attributes and extract all values + all_values = [] + for nested_attr in var_data['Attribute']: + if isinstance(nested_attr, dict) and 'Value' in nested_attr: + nested_values = nested_attr.get('Value', []) + if isinstance(nested_values, list): + all_values.extend(nested_values) + else: + all_values.append(nested_values) + + attribute.append({ + "Variable": variable_name, + "Attribute": attr_name, + "Value": all_values + }) + else: + # Fallback to old behavior for backwards compatibility + attribute.append({ + "Variable": variable_name, + "Attribute": attr_name, + "Value": var_data.get("Value", "") + }) # Format choices as the math_reason prompt expects formatted_choices = [f"({i}) {choice}" for i, choice in enumerate(choices)] @@ -100,15 +153,15 @@ def run(self, scenario_state, choices, actions, mathematical_model=None, attribu # Generate target bias for prompt with high/low determination if alignment_target and target_attributes: bias_parts = [] - for attribute in target_attributes: - # Use attribute.kdma (not attribute.name) to match alignment_target kdma_values - attr_value = kdma_value_dict.get(attribute.kdma) + for target_attr in target_attributes: + # Use target_attr.kdma (not target_attr.name) to match alignment_target kdma_values + attr_value = kdma_value_dict.get(target_attr.kdma) if attr_value is not None and attr_value >= 0.5: - bias_parts.append(f"high {attribute.name}") + bias_parts.append(f"high {target_attr.name}") elif attr_value is not None: - bias_parts.append(f"low {attribute.name}") + bias_parts.append(f"low {target_attr.name}") else: - bias_parts.append(f"low {attribute.name}") # Conservative default + bias_parts.append(f"low {target_attr.name}") # Conservative default target_bias = f"Focus on {', '.join(bias_parts)} when making decisions." else: target_bias = "Make decisions based on medical triage best practices." @@ -146,23 +199,40 @@ def run(self, scenario_state, choices, actions, mathematical_model=None, attribu dialog_prompt = self.structured_inference_engine.dialog_to_prompt(dialog) + log.info("=" * 80) + log.info("MathReason Dialog Prompt") + log.info("=" * 80) + log.info(dialog_prompt) + log.info("=" * 80) + # Retry loop for structured inference with validation response = None last_error = None + import time + inference_start = time.time() + for attempt in range(self.max_json_retries): try: # Run structured inference + log.info(f"Running inference attempt {attempt + 1}/{self.max_json_retries}...") raw_response = self.structured_inference_engine.run_inference( dialog_prompt, output_schema ) + log.info("=" * 80) + log.info("MathReason Raw Response") + log.info("=" * 80) + log.info(f"{raw_response}") + log.info("=" * 80) + # Validate response response = validate_structured_response(raw_response) # Success - break out of retry loop - log.info(f"MathReason stage inference succeeded on attempt {attempt + 1}") + inference_elapsed = time.time() - inference_start + log.info(f"MathReason stage inference succeeded on attempt {attempt + 1} (took {inference_elapsed:.2f}s)") break except (json.JSONDecodeError, ValueError, TypeError) as e: @@ -186,6 +256,13 @@ def run(self, scenario_state, choices, actions, mathematical_model=None, attribu reasoning = response.get('Reasoning', '') answer_idx = response.get('Answer', 0) + log.info("=" * 80) + log.info("MathReason Parsed Response") + log.info("=" * 80) + log.info(f"Answer Index: {answer_idx}") + log.info(f"Reasoning: {reasoning}") + log.info("=" * 80) + # Validate answer index if not isinstance(answer_idx, int) or answer_idx < 0 or answer_idx >= len(actions): log.warning(f"Invalid answer index {answer_idx}, defaulting to 0") @@ -197,7 +274,21 @@ def run(self, scenario_state, choices, actions, mathematical_model=None, attribu if hasattr(chosen_action, 'justification') and chosen_action.justification is None: chosen_action.justification = reasoning - log.info(f"MathReason stage completed: Selected action {answer_idx}") + log.info("=" * 80) + log.info("MathReason Final Selection") + log.info("=" * 80) + log.info(f"Selected action index: {answer_idx}") + log.info(f"Selected choice: {choices[answer_idx] if answer_idx < len(choices) else 'N/A'}") + if hasattr(chosen_action, 'action_id'): + log.info(f"Action ID: {chosen_action.action_id}") + if hasattr(chosen_action, 'character_id'): + log.info(f"Character ID: {chosen_action.character_id}") + if hasattr(chosen_action, 'unstructured'): + log.info(f"Unstructured: {chosen_action.unstructured}") + log.info(f"Justification: {reasoning[:200]}{'...' if len(reasoning) > 200 else ''}") + log.info("=" * 80) + log.info("MathReason Stage Completed Successfully") + log.info("=" * 80) return chosen_action diff --git a/align_system/algorithms/decision_flow_adm/objective_stage_component.py b/align_system/algorithms/decision_flow_adm/objective_stage_component.py index b0bff3bf..3761bd22 100644 --- a/align_system/algorithms/decision_flow_adm/objective_stage_component.py +++ b/align_system/algorithms/decision_flow_adm/objective_stage_component.py @@ -1,34 +1,24 @@ -import json -from align_system.utils import logging, call_with_coerced_args +from align_system.utils import logging from align_system.algorithms.abstracts import ADMComponent -from align_system.utils.alignment_utils import attributes_in_alignment_target -from align_system.data_models.dialog import DialogElement -from align_system.algorithms.decision_flow_adm.utils import validate_structured_response -from align_system.exceptions import SceneSkipException log = logging.getLogger(__name__) class ObjectiveStageComponent(ADMComponent): + """Objective stage component that filters weighted pairs and builds objective function. + + Following DecisionFlow reference: this stage does NOT make an LLM call. + It simply filters Variable-Attribute pairs by weight threshold and + constructs the objective function string programmatically. + """ + def __init__( self, - structured_inference_engine, - scenario_description_template, - system_prompt_template, - prompt_template, - output_schema_template, - max_json_retries=5, - attributes=None, weight_threshold=0.3, + attributes=None, **kwargs, ): - self.structured_inference_engine = structured_inference_engine - self.scenario_description_template = scenario_description_template - self.system_prompt_template = system_prompt_template - self.prompt_template = prompt_template - self.output_schema_template = output_schema_template self.weight_threshold = weight_threshold - self.max_json_retries = max_json_retries if attributes is None: attributes = {} @@ -37,143 +27,62 @@ def __init__( def run_returns(self): return "objective_function" - def run(self, scenario_state, choices, filter_analysis=None, attribute_analysis=None, variables=None, extraction=None, alignment_target=None, **kwargs): - """Create objective function by filtering attributes with weights above threshold""" + def run(self, filter_analysis=None, **kwargs): + """Create objective function from Variable-Attribute pairs that exceed weight threshold. - # Handle alignment_target workflow similar to other stage components - if alignment_target is None: - # No alignment target - use all attributes - target_attributes = list(self.attributes.values()) - else: - # Alignment target provided - ONLY use attributes in the alignment target - target_attribute_names = attributes_in_alignment_target(alignment_target) - target_attributes = [self.attributes[n] for n in target_attribute_names if n in self.attributes] + Following DecisionFlow reference: this stage does NOT make an LLM call. + It filters pairs by weight threshold and builds the objective function string. + """ - objective_components = [] + log.info("=" * 60) + log.info("Objective Stage: Building objective function from filtered pairs") + log.info("=" * 60) - # Process filter analysis to identify high-weight attributes for objective function - # Following the logic from decision_flow_stages.py lines 132-156 - if filter_analysis: - for attribute in target_attributes: - attribute_name = attribute.name - if attribute_name in filter_analysis: - weight = filter_analysis[attribute_name].get('weight', 0) - explanation = filter_analysis[attribute_name].get('explanation', '') + # Collect all Variable-Attribute pairs that exceed weight threshold + filtered_pairs = [] - # Apply weight threshold filter (similar to decision_flow_stages.py line 137) - if weight > self.weight_threshold: - log.info(f"Including attribute {attribute_name} with weight {weight} in objective function") + if filter_analysis: + for kdma_name, kdma_data in filter_analysis.items(): + weighted_pairs = kdma_data.get('weighted_pairs', []) - # Get attribute analysis data for this attribute - attribute_data = attribute_analysis.get(attribute_name, []) if attribute_analysis else [] + for pair in weighted_pairs: + weight = pair.get('Weight', 0) - objective_component = { - "Variable": attribute_name, - "Attribute": attribute.name, + if weight > self.weight_threshold: + log.info(f"Including: {pair.get('Variable', '')[:30]}... - {pair.get('Attribute', '')} (weight={weight})") + filtered_pairs.append({ + "Variable": pair.get('Variable', ''), + "Attribute": pair.get('Attribute', ''), + "Value": pair.get('Value', []), "Weight": weight, - "Explanation": explanation, - "AttributeData": attribute_data - } - objective_components.append(objective_component) + "Explanation": pair.get('Explanation', ''), + "KDMA": kdma_name + }) else: - log.info(f"Excluding attribute {attribute_name} with weight {weight} (below threshold {self.weight_threshold})") + log.info(f"Excluding: {pair.get('Variable', '')[:30]}... - {pair.get('Attribute', '')} (weight={weight} < {self.weight_threshold})") - # Create objective function string following decision_flow_stages.py format (lines 148-153) + log.info(f"Total filtered pairs admitted to objective: {len(filtered_pairs)}") + + # Create objective function string from filtered pairs + # Format: weight * (attribute) of (variable) + ... objective_function_text = "The final formula to be calculated is " - if objective_components: - for component in objective_components: - variable = component["Variable"] - weight = component["Weight"] - attribute = component["Attribute"] + if filtered_pairs: + for pair in filtered_pairs: + variable = pair["Variable"] + attribute = pair["Attribute"] + weight = pair["Weight"] objective_function_text += f"{weight} * ({attribute}) of ({variable}) + " # Remove trailing " + " objective_function_text = objective_function_text.rstrip(" + ") else: - # Fallback if no components meet threshold (line 156) + # Fallback if no components meet threshold objective_function_text = "weight * attribute of variable" log.info(f"Generated objective function: {objective_function_text}") - - scenario_description = call_with_coerced_args( - self.scenario_description_template, - { - 'scenario_state': scenario_state, - 'alignment_target': alignment_target - }) - - dialog = [] - if self.system_prompt_template is not None: - system_prompt = call_with_coerced_args( - self.system_prompt_template, - {'objective_components': objective_components} - ) - - dialog.insert(0, DialogElement(role='system', - content=system_prompt)) - - log.info(f"Creating objective function with {len(objective_components)} components") - - prompt = call_with_coerced_args( - self.prompt_template, - { - 'scenario_description': scenario_description, - 'choices': choices, - 'objective_components': objective_components, - 'objective_function_text': objective_function_text, - 'weight_threshold': self.weight_threshold - }, - ) - log.info(f"Objective prompt: {prompt}") - - dialog.append(DialogElement(role='user', - content=prompt)) - - output_schema = call_with_coerced_args( - self.output_schema_template, - {}) - - dialog_prompt = self.structured_inference_engine.dialog_to_prompt(dialog) - - # Retry loop for structured inference with validation - response = None - last_error = None - - for attempt in range(self.max_json_retries): - try: - # Run structured inference - raw_response = self.structured_inference_engine.run_inference( - dialog_prompt, - output_schema - ) - - # Validate response - response = validate_structured_response(raw_response) - - # Success - break out of retry loop - log.info(f"Objective stage inference succeeded on attempt {attempt + 1}") - break - - except (json.JSONDecodeError, ValueError, TypeError) as e: - last_error = e - log.warning( - f"Objective stage JSON decode error on attempt {attempt + 1}/{self.max_json_retries}: {e}" - ) - - if attempt < self.max_json_retries - 1: - log.info("Retrying Objective stage inference...") - else: - log.error(f"Objective stage failed after {self.max_json_retries} attempts") - raise SceneSkipException( - f"Failed to generate valid JSON after {self.max_json_retries} attempts. " - f"Last error: {last_error}", - component_name="ObjectiveStageComponent", - last_error=last_error - ) from last_error - - log.info(f"Objective function creation completed: {response.get('objective_function', objective_function_text)}") + log.info("Objective Stage Completed (no LLM call - matches DecisionFlow reference)") return { - 'objective_function': response.get('objective_function', objective_function_text), - 'components': objective_components, + 'objective_function': objective_function_text, + 'filtered_pairs': filtered_pairs, 'weight_threshold_used': self.weight_threshold } diff --git a/align_system/algorithms/decision_flow_adm/utils.py b/align_system/algorithms/decision_flow_adm/utils.py index bfd5d531..7950e275 100644 --- a/align_system/algorithms/decision_flow_adm/utils.py +++ b/align_system/algorithms/decision_flow_adm/utils.py @@ -16,9 +16,15 @@ _COMMA_FIX_1 = re.compile(r'"\s+"') _COMMA_FIX_2 = re.compile(r'"\s+(?=["[])') +# Repair patterns for common JSON errors +_TRAILING_COMMA = re.compile(r',(\s*[}\]])') # Trailing comma before } or ] + # Key-value extraction pattern _KV_PATTERN = re.compile(r'"([^"]+)":\s*(?:"([^"]*)",?|(-?\d+)[,}\s\n])') +# Array extraction pattern (handles arrays of strings) +_ARRAY_PATTERN = re.compile(r'"([^"]+)":\s*\[([^\]]*)\]', re.DOTALL) + def validate_structured_response(response: Any) -> Dict[str, Any]: """ @@ -144,6 +150,43 @@ def extract_json_from_text( return None +def repair_json_string(json_str: str) -> str: + """ + Attempt to repair common JSON syntax errors. + + Fixes: + - Trailing commas before ] or } + - Newlines inside string values + - Truncated JSON (missing closing braces/brackets) + + Args: + json_str: JSON string that may have syntax errors + + Returns: + Repaired JSON string + """ + if not json_str: + return json_str + + # Remove trailing commas before ] or } + json_str = _TRAILING_COMMA.sub(r'\1', json_str) + + # Replace actual newlines inside strings with spaces + # This is a simplified approach - handles most cases + json_str = json_str.replace('\n', ' ').replace('\r', ' ') + + # Fix truncated JSON by adding missing closing braces/brackets + open_braces = json_str.count('{') - json_str.count('}') + open_brackets = json_str.count('[') - json_str.count(']') + + if open_brackets > 0: + json_str += '"]' * open_brackets # Assume array of strings was truncated + if open_braces > 0: + json_str += '}' * open_braces + + return json_str + + def clean_json_string(json_str: str) -> str: """ Clean a JSON string by removing trailing characters, fixing newlines, and adding missing commas. @@ -161,7 +204,8 @@ def clean_json_string(json_str: str) -> str: # Fast path: already clean JSON (common case) if json_str[0] == '{' and json_str[-1] == '}' and '' not in json_str: - return json_str + # Still try to repair even if it looks clean + return repair_json_string(json_str) # Remove trailing tokens like (only if needed) if '' in json_str: @@ -181,32 +225,46 @@ def clean_json_string(json_str: str) -> str: output = _COMMA_FIX_1.sub('", "', output) output = _COMMA_FIX_2.sub('", ', output) + # Apply repair as final step + output = repair_json_string(output) + return output @lru_cache(maxsize=128) def _parse_all_keys_once(text: str) -> Dict[str, Any]: """ - Parse text once and extract all key-value pairs using a single regex pass. + Parse text once and extract all key-value pairs using regex passes. This is O(n) instead of O(kxn) where k is the number of keys. LRU cache provides additional speedup for repeated calls with same text. + Extracts both simple key-value pairs and arrays of strings. + Args: text: Text that may contain key-value pairs Returns: - Dict containing all found key-value pairs + Dict containing all found key-value pairs (including arrays) """ result = {} - # Single regex pass to find all key-value pairs at once + # Extract arrays first (higher priority) + for match in _ARRAY_PATTERN.finditer(text): + key, array_content = match.groups() + # Parse array items (strings) + items = re.findall(r'"([^"]*)"', array_content) + if items: + result[key] = items + + # Extract simple key-value pairs (won't overwrite arrays) for match in _KV_PATTERN.finditer(text): key, str_val, num_val = match.groups() - if str_val is not None: - result[key] = str_val.strip() - elif num_val is not None: - result[key] = int(num_val) + if key not in result: # Don't overwrite array values + if str_val is not None: + result[key] = str_val.strip() + elif num_val is not None: + result[key] = int(num_val) return result diff --git a/align_system/configs/adm/outlines_decision_flow_aligned/phase2_pipeline_decision_flow_fine_grained.yaml b/align_system/configs/adm/outlines_decision_flow_aligned/phase2_pipeline_decision_flow_fine_grained.yaml new file mode 100644 index 00000000..f26499ed --- /dev/null +++ b/align_system/configs/adm/outlines_decision_flow_aligned/phase2_pipeline_decision_flow_fine_grained.yaml @@ -0,0 +1,119 @@ +name: phase2_pipeline_decision_flow_fine_grained + +defaults: + # Import defaults into this namspace (adm) as @name, for further + # customization + + # Shared variables / components + - /attribute@mu: medical_urgency + - /attribute@af: affiliation_focus + - /attribute@mf: merit_focus + - /attribute@ss: search_or_stay + - /attribute@ps: personal_safety + - /attribute@pf: naacl24/protocol_focus + - /attribute@ff: naacl24/fairness + - /attribute@uc: naacl24/utilitarianism + - /attribute@cc: naacl24/continuing_care + - /attribute@md: naacl24/moral_desert + - /attribute@ra: naacl24/risk_aversion + - /inference_engine@structured_inference_engine: outlines_structured_multinomial_constrained + # ADM components to be used in "steps" + - /adm_component/misc@step_definitions.format_choices: itm_format_choices + - /adm_component/decision_flow@step_definitions.variables: variables + - /adm_component/decision_flow@step_definitions.extraction: extraction + - /adm_component/decision_flow@step_definitions.phase2_attribute_fine_grained: phase2_attribute_fine_grained + - /adm_component/decision_flow@step_definitions.filter: filter + - /adm_component/decision_flow@step_definitions.objective: objective + # - /adm_component/decision_flow@step_definitions.express: express + - /adm_component/decision_flow@step_definitions.express_unstructured: express_unstructured + - /adm_component/decision_flow@step_definitions.math_reason_fine_grained: math_reason_fine_grained + - /adm_component/misc@step_definitions.ensure_chosen_action: ensure_chosen_action + - /adm_component/misc@step_definitions.populate_choice_info: populate_choice_info + # Prompt templates to be used in Variables Extraction Phase + - /template/scenario_description@scenario_description_template: phase2 + - /template/prompt/decision_flow@prompt_template: variables + - /template/output_schema/decision_flow@variables_output_schema: variables + # Prompt templates to be used in Extraction Phase + - /template/prompt/decision_flow@extraction_prompt_template: extraction + - /template/output_schema/decision_flow@extraction_output_schema: extraction + # Prompt templates to be used in Attribute Fine-Grained Phase + - /template/prompt/decision_flow@attribute_fine_grained_prompt_template: phase2_attribute_fine_grained + - /template/output_schema/decision_flow@attribute_fine_grained_output_schema: phase2_attribute_fine_grained + # Prompt templates to be used in Filter Phase + - /template/prompt/decision_flow@filter_prompt_template: filter + - /template/output_schema/decision_flow@filter_output_schema: filter + # NOTE: Objective Phase does NOT use prompts (matches DecisionFlow reference) + # Prompt templates to be used in Express Phase + - /template/prompt/decision_flow@express_prompt_template: express + - /template/output_schema/decision_flow@express_output_schema: express + # Prompt templates to be used in MathReason Fine-Grained Phase + - /template/prompt/decision_flow@math_reason_fine_grained_prompt_template: math_reason_fine_grained + - /template/output_schema/decision_flow@math_reason_fine_grained_output_schema: math_reason_fine_grained + # Use definitions in this file to override defaults defined above + - _self_ + +attribute_definitions: + medical: ${adm.mu} + affiliation: ${adm.af} + merit: ${adm.mf} + search: ${adm.ss} + personal_safety: ${adm.ps} + ProtocolFocus: ${adm.pf} + Fairness: ${adm.ff} + Utilitarianism: ${adm.uc} + ContinuationOfCare: ${adm.cc} + MoralDesert: ${adm.md} + RiskAversion: ${adm.ra} + +step_definitions: + variables: + scenario_description_template: ${ref:adm.scenario_description_template} + prompt_template: ${ref:adm.prompt_template} + output_schema_template: ${ref:adm.variables_output_schema} + extraction: + scenario_description_template: ${ref:adm.scenario_description_template} + prompt_template: ${ref:adm.extraction_prompt_template} + output_schema_template: ${ref:adm.extraction_output_schema} + phase2_attribute_fine_grained: + scenario_description_template: ${ref:adm.scenario_description_template} + prompt_template: ${ref:adm.attribute_fine_grained_prompt_template} + output_schema_template: ${ref:adm.attribute_fine_grained_output_schema} + attributes: ${adm.attribute_definitions} + filter: + scenario_description_template: ${ref:adm.scenario_description_template} + prompt_template: ${ref:adm.filter_prompt_template} + output_schema_template: ${ref:adm.filter_output_schema} + attributes: ${adm.attribute_definitions} + objective: + # NOTE: No prompts needed - Objective stage doesn't make LLM calls + attributes: ${adm.attribute_definitions} + # express: + # scenario_description_template: ${ref:adm.scenario_description_template} + # prompt_template: ${ref:adm.express_prompt_template} + # output_schema_template: ${ref:adm.express_output_schema} + express_unstructured: + prompt_template: ${ref:adm.express_prompt_template} + math_reason_fine_grained: + structured_inference_engine: ${ref:adm.structured_inference_engine} + scenario_description_template: ${ref:adm.scenario_description_template} + prompt_template: ${ref:adm.math_reason_fine_grained_prompt_template} + output_schema_template: ${ref:adm.math_reason_fine_grained_output_schema} + attributes: ${adm.attribute_definitions} + + +instance: + _target_: align_system.algorithms.pipeline_adm.PipelineADM + + steps: + # Reference the step instances we want to use in order + - ${ref:adm.step_definitions.format_choices} + - ${ref:adm.step_definitions.variables} + - ${ref:adm.step_definitions.extraction} + - ${ref:adm.step_definitions.phase2_attribute_fine_grained} + - ${ref:adm.step_definitions.filter} + - ${ref:adm.step_definitions.objective} + # - ${ref:adm.step_definitions.express} + - ${ref:adm.step_definitions.express_unstructured} + - ${ref:adm.step_definitions.math_reason_fine_grained} + - ${ref:adm.step_definitions.ensure_chosen_action} + - ${ref:adm.step_definitions.populate_choice_info} diff --git a/align_system/configs/adm/outlines_decision_flow_aligned/pipeline_decision_flow.yaml b/align_system/configs/adm/outlines_decision_flow_aligned/pipeline_decision_flow.yaml index b772f703..308eb8db 100644 --- a/align_system/configs/adm/outlines_decision_flow_aligned/pipeline_decision_flow.yaml +++ b/align_system/configs/adm/outlines_decision_flow_aligned/pipeline_decision_flow.yaml @@ -42,9 +42,7 @@ defaults: # Prompt templates to be used in Filter Phase - /template/prompt/decision_flow@filter_prompt_template: filter - /template/output_schema/decision_flow@filter_output_schema: filter - # Prompt templates to be used in Objective Phase - - /template/prompt/decision_flow@objective_prompt_template: objective - - /template/output_schema/decision_flow@objective_output_schema: objective + # NOTE: Objective Phase does NOT use prompts (matches DecisionFlow reference) # Prompt templates to be used in Express Phase - /template/prompt/decision_flow@express_prompt_template: express - /template/output_schema/decision_flow@express_output_schema: express @@ -87,16 +85,13 @@ step_definitions: output_schema_template: ${ref:adm.filter_output_schema} attributes: ${adm.attribute_definitions} objective: - scenario_description_template: ${ref:adm.scenario_description_template} - prompt_template: ${ref:adm.objective_prompt_template} - output_schema_template: ${ref:adm.objective_output_schema} + # NOTE: No prompts needed - Objective stage doesn't make LLM calls attributes: ${adm.attribute_definitions} # express: # scenario_description_template: ${ref:adm.scenario_description_template} # prompt_template: ${ref:adm.express_prompt_template} # output_schema_template: ${ref:adm.express_output_schema} express_unstructured: - scenario_description_template: ${ref:adm.scenario_description_template} prompt_template: ${ref:adm.express_prompt_template} math_reason: structured_inference_engine: ${ref:adm.structured_inference_engine} diff --git a/align_system/configs/adm_component/decision_flow/express_unstructured.yaml b/align_system/configs/adm_component/decision_flow/express_unstructured.yaml index 5e40faff..5819ef81 100644 --- a/align_system/configs/adm_component/decision_flow/express_unstructured.yaml +++ b/align_system/configs/adm_component/decision_flow/express_unstructured.yaml @@ -1,8 +1,7 @@ _target_: align_system.algorithms.decision_flow_adm.express_stage_unstructured.ExpressStageUnstructuredComponent structured_inference_engine: ${ref:adm.structured_inference_engine} -num_samples: 1 -attributes: ${ref:adm.attribute_definitions} max_json_retries: 100 +attributes: ${ref:adm.attribute_definitions} system_prompt_template: _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt diff --git a/align_system/configs/adm_component/decision_flow/math_reason_fine_grained.yaml b/align_system/configs/adm_component/decision_flow/math_reason_fine_grained.yaml new file mode 100644 index 00000000..d07a725c --- /dev/null +++ b/align_system/configs/adm_component/decision_flow/math_reason_fine_grained.yaml @@ -0,0 +1,9 @@ +_target_: align_system.algorithms.decision_flow_adm.math_reason_fine_grained_stage_component.MathReasonFineGrainedStageComponent + +structured_inference_engine: ??? +scenario_description_template: ??? +system_prompt_template: ??? +prompt_template: ${template.prompt.decision_flow.math_reason_fine_grained} +output_schema_template: ${template.output_schema.decision_flow.math_reason_fine_grained} +attributes: ??? +max_json_retries: 5 diff --git a/align_system/configs/adm_component/decision_flow/objective.yaml b/align_system/configs/adm_component/decision_flow/objective.yaml index 21a9a300..1d1f1d46 100644 --- a/align_system/configs/adm_component/decision_flow/objective.yaml +++ b/align_system/configs/adm_component/decision_flow/objective.yaml @@ -1,9 +1,6 @@ +# Objective stage: Filters weighted pairs and builds objective function string +# NOTE: This stage does NOT make an LLM call (matches DecisionFlow reference) _target_: align_system.algorithms.decision_flow_adm.objective_stage_component.ObjectiveStageComponent -structured_inference_engine: ${ref:adm.structured_inference_engine} -num_samples: 1 attributes: ${ref:adm.attribute_definitions} -weight_threshold: 0.3 -max_json_retries: 5 -system_prompt_template: - _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt \ No newline at end of file +weight_threshold: 0.3 \ No newline at end of file diff --git a/align_system/configs/adm_component/decision_flow/phase2_attribute_fine_grained.yaml b/align_system/configs/adm_component/decision_flow/phase2_attribute_fine_grained.yaml new file mode 100644 index 00000000..bf980790 --- /dev/null +++ b/align_system/configs/adm_component/decision_flow/phase2_attribute_fine_grained.yaml @@ -0,0 +1,8 @@ +_target_: align_system.algorithms.decision_flow_adm.attribute_fine_grained_stage_component.AttributeFineGrainedStageComponent + +structured_inference_engine: ${ref:adm.structured_inference_engine} +num_samples: 1 +attributes: ${ref:adm.attribute_definitions} +max_json_retries: 5 +system_prompt_template: + _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt diff --git a/align_system/configs/experiment/examples/phase2_pipeline_decision_flow_fine_grained.yaml b/align_system/configs/experiment/examples/phase2_pipeline_decision_flow_fine_grained.yaml new file mode 100644 index 00000000..bdfb3107 --- /dev/null +++ b/align_system/configs/experiment/examples/phase2_pipeline_decision_flow_fine_grained.yaml @@ -0,0 +1,64 @@ +# @package _global_ +defaults: + - override /adm: outlines_decision_flow_aligned/phase2_pipeline_decision_flow_fine_grained + - override /inference_engine@adm.structured_inference_engine: outlines_structured_multinomial_constrained + - override /interface: ta3 + +interface: + session_type: adept + training_session: full + username: "pipeline_decision_flow_fine_grained" + domain: "p2triage" + +adm: + step_definitions: + variables: + num_samples: 1 + system_prompt_template: + _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt + extraction: + num_samples: 1 + system_prompt_template: + _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt + phase2_attribute_fine_grained: + num_samples: 1 + system_prompt_template: + _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt + filter: + num_samples: 1 + system_prompt_template: + _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt + objective: + num_samples: 1 + system_prompt_template: + _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt + # express: + # num_samples: 1 + # system_prompt_template: + # _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt + express_unstructured: + system_prompt_template: + _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt + math_reason_fine_grained: + system_prompt_template: + _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt + + instance: + steps: + - ${ref:adm.step_definitions.format_choices} + - ${ref:adm.step_definitions.variables} + - ${ref:adm.step_definitions.extraction} + - ${ref:adm.step_definitions.phase2_attribute_fine_grained} + - ${ref:adm.step_definitions.filter} + - ${ref:adm.step_definitions.objective} + # - ${ref:adm.step_definitions.express} + - ${ref:adm.step_definitions.express_unstructured} + - ${ref:adm.step_definitions.math_reason_fine_grained} + - ${ref:adm.step_definitions.ensure_chosen_action} + - ${ref:adm.step_definitions.populate_choice_info} + +align_to_target: true + +hydra: + run: + dir: './decision_flow_fine_grained/${now:%Y-%m-%d__%H-%M-%S}' diff --git a/align_system/configs/experiment/examples/pipeline_decision_flow.yaml b/align_system/configs/experiment/examples/pipeline_decision_flow.yaml index c186eeb0..e919b3d6 100644 --- a/align_system/configs/experiment/examples/pipeline_decision_flow.yaml +++ b/align_system/configs/experiment/examples/pipeline_decision_flow.yaml @@ -30,7 +30,6 @@ adm: # system_prompt_template: # _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt express_unstructured: - num_samples: 1 system_prompt_template: _target_: align_system.prompt_engineering.outlines_prompts.DefaultITMBaselineSystemPrompt math_reason: diff --git a/align_system/configs/template/output_schema/decision_flow/attribute.yaml b/align_system/configs/template/output_schema/decision_flow/attribute.yaml index bbebd2db..4f09248d 100644 --- a/align_system/configs/template/output_schema/decision_flow/attribute.yaml +++ b/align_system/configs/template/output_schema/decision_flow/attribute.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.AttributeOutputSchema \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.AttributeOutputSchema \ No newline at end of file diff --git a/align_system/configs/template/output_schema/decision_flow/express.yaml b/align_system/configs/template/output_schema/decision_flow/express.yaml index 2adc6ef7..c51095c1 100644 --- a/align_system/configs/template/output_schema/decision_flow/express.yaml +++ b/align_system/configs/template/output_schema/decision_flow/express.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.ExpressOutputSchema \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.ExpressOutputSchema \ No newline at end of file diff --git a/align_system/configs/template/output_schema/decision_flow/extraction.yaml b/align_system/configs/template/output_schema/decision_flow/extraction.yaml index 226b4caf..5a3f9834 100644 --- a/align_system/configs/template/output_schema/decision_flow/extraction.yaml +++ b/align_system/configs/template/output_schema/decision_flow/extraction.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.ExtractionOutputSchema +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.ExtractionOutputSchema diff --git a/align_system/configs/template/output_schema/decision_flow/filter.yaml b/align_system/configs/template/output_schema/decision_flow/filter.yaml index 196c57fd..c17b0e2a 100644 --- a/align_system/configs/template/output_schema/decision_flow/filter.yaml +++ b/align_system/configs/template/output_schema/decision_flow/filter.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.FilterOutputSchema \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.FilterOutputSchema \ No newline at end of file diff --git a/align_system/configs/template/output_schema/decision_flow/math_reason.yaml b/align_system/configs/template/output_schema/decision_flow/math_reason.yaml index 250ba64e..02724eff 100644 --- a/align_system/configs/template/output_schema/decision_flow/math_reason.yaml +++ b/align_system/configs/template/output_schema/decision_flow/math_reason.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.MathReasonOutputSchema \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.MathReasonOutputSchema \ No newline at end of file diff --git a/align_system/configs/template/output_schema/decision_flow/math_reason_fine_grained.yaml b/align_system/configs/template/output_schema/decision_flow/math_reason_fine_grained.yaml new file mode 100644 index 00000000..59b0197c --- /dev/null +++ b/align_system/configs/template/output_schema/decision_flow/math_reason_fine_grained.yaml @@ -0,0 +1 @@ +_target_: align_system.prompt_engineering.decision_flow.fine_grained_prompts.FineGrainedMathReasonOutputSchema diff --git a/align_system/configs/template/output_schema/decision_flow/objective.yaml b/align_system/configs/template/output_schema/decision_flow/objective.yaml index 463ed7af..78e065d2 100644 --- a/align_system/configs/template/output_schema/decision_flow/objective.yaml +++ b/align_system/configs/template/output_schema/decision_flow/objective.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.ObjectiveOutputSchema \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.ObjectiveOutputSchema \ No newline at end of file diff --git a/align_system/configs/template/output_schema/decision_flow/phase2_attribute_fine_grained.yaml b/align_system/configs/template/output_schema/decision_flow/phase2_attribute_fine_grained.yaml new file mode 100644 index 00000000..2713ee59 --- /dev/null +++ b/align_system/configs/template/output_schema/decision_flow/phase2_attribute_fine_grained.yaml @@ -0,0 +1 @@ +_target_: align_system.prompt_engineering.decision_flow.fine_grained_prompts.Phase2FineGrainedAttributeOutputSchema diff --git a/align_system/configs/template/output_schema/decision_flow/variables.yaml b/align_system/configs/template/output_schema/decision_flow/variables.yaml index 0c1ab0d5..3e48af05 100644 --- a/align_system/configs/template/output_schema/decision_flow/variables.yaml +++ b/align_system/configs/template/output_schema/decision_flow/variables.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.VariablesOutputSchema +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.VariablesOutputSchema diff --git a/align_system/configs/template/prompt/decision_flow/attribute.yaml b/align_system/configs/template/prompt/decision_flow/attribute.yaml index 55687b32..42a05993 100644 --- a/align_system/configs/template/prompt/decision_flow/attribute.yaml +++ b/align_system/configs/template/prompt/decision_flow/attribute.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.AttributePrompt \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.AttributePrompt \ No newline at end of file diff --git a/align_system/configs/template/prompt/decision_flow/express.yaml b/align_system/configs/template/prompt/decision_flow/express.yaml index 489873a8..cb72d8c3 100644 --- a/align_system/configs/template/prompt/decision_flow/express.yaml +++ b/align_system/configs/template/prompt/decision_flow/express.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.ExpressPrompt \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.ExpressPrompt \ No newline at end of file diff --git a/align_system/configs/template/prompt/decision_flow/extraction.yaml b/align_system/configs/template/prompt/decision_flow/extraction.yaml index 0e668df9..7bccd148 100644 --- a/align_system/configs/template/prompt/decision_flow/extraction.yaml +++ b/align_system/configs/template/prompt/decision_flow/extraction.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.ExtractionPrompt +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.ExtractionPrompt diff --git a/align_system/configs/template/prompt/decision_flow/filter.yaml b/align_system/configs/template/prompt/decision_flow/filter.yaml index 3ba8a0e6..fec998ef 100644 --- a/align_system/configs/template/prompt/decision_flow/filter.yaml +++ b/align_system/configs/template/prompt/decision_flow/filter.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.FilterPrompt \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.FilterPrompt \ No newline at end of file diff --git a/align_system/configs/template/prompt/decision_flow/math_reason.yaml b/align_system/configs/template/prompt/decision_flow/math_reason.yaml index d0935dcf..d5ec59fd 100644 --- a/align_system/configs/template/prompt/decision_flow/math_reason.yaml +++ b/align_system/configs/template/prompt/decision_flow/math_reason.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.MathReasonPrompt \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.MathReasonPrompt \ No newline at end of file diff --git a/align_system/configs/template/prompt/decision_flow/math_reason_fine_grained.yaml b/align_system/configs/template/prompt/decision_flow/math_reason_fine_grained.yaml new file mode 100644 index 00000000..41f5822f --- /dev/null +++ b/align_system/configs/template/prompt/decision_flow/math_reason_fine_grained.yaml @@ -0,0 +1 @@ +_target_: align_system.prompt_engineering.decision_flow.fine_grained_prompts.FineGrainedMathReasonPrompt diff --git a/align_system/configs/template/prompt/decision_flow/objective.yaml b/align_system/configs/template/prompt/decision_flow/objective.yaml index 5b2dc840..b3938aed 100644 --- a/align_system/configs/template/prompt/decision_flow/objective.yaml +++ b/align_system/configs/template/prompt/decision_flow/objective.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.ObjectivePrompt \ No newline at end of file +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.ObjectivePrompt \ No newline at end of file diff --git a/align_system/configs/template/prompt/decision_flow/phase2_attribute_fine_grained.yaml b/align_system/configs/template/prompt/decision_flow/phase2_attribute_fine_grained.yaml new file mode 100644 index 00000000..9b0e4163 --- /dev/null +++ b/align_system/configs/template/prompt/decision_flow/phase2_attribute_fine_grained.yaml @@ -0,0 +1 @@ +_target_: align_system.prompt_engineering.decision_flow.fine_grained_prompts.Phase2FineGrainedAttributePrompt diff --git a/align_system/configs/template/prompt/decision_flow/variables.yaml b/align_system/configs/template/prompt/decision_flow/variables.yaml index 2b04fca5..d88b2275 100644 --- a/align_system/configs/template/prompt/decision_flow/variables.yaml +++ b/align_system/configs/template/prompt/decision_flow/variables.yaml @@ -1 +1 @@ -_target_: align_system.prompt_engineering.outlines_prompts.VariablesPrompt +_target_: align_system.prompt_engineering.decision_flow.high_low_prompts.VariablesPrompt diff --git a/align_system/prompt_engineering/decision_flow/__init__.py b/align_system/prompt_engineering/decision_flow/__init__.py new file mode 100644 index 00000000..f91de1f8 --- /dev/null +++ b/align_system/prompt_engineering/decision_flow/__init__.py @@ -0,0 +1,57 @@ +""" +DecisionFlow Prompt Engineering Module +======================================== + +This module contains prompt templates and schemas for the DecisionFlow pipeline. + +Main modules: +- high_low_prompts: Binary high/low value prompts for all DecisionFlow stages +- fine_grained_prompts: Fine-grained value prompts with explicit scale anchors (Attribute + MathReason stages) +""" + +from align_system.prompt_engineering.decision_flow.high_low_prompts import ( + # Variables Stage + VariablesPrompt, + VariablesOutputSchema, + # Extraction Stage + ExtractionPrompt, + ExtractionOutputSchema, + # Attribute Stage (High/Low) + AttributePrompt, + AttributeOutputSchema, + # Filter Stage + FilterPrompt, + FilterOutputSchema, + # Objective Stage + ObjectivePrompt, + ObjectiveOutputSchema, + # Express Stage + ExpressPrompt, + ExpressOutputSchema, + # MathReason Stage + MathReasonPrompt, + MathReasonOutputSchema, +) + +from align_system.prompt_engineering.decision_flow.fine_grained_prompts import ( + # Phase 2 Fine-Grained Attribute Stage + Phase2FineGrainedAttributePrompt, + Phase2FineGrainedAttributeOutputSchema, + # Fine-Grained MathReason Stage + FineGrainedMathReasonPrompt, + FineGrainedMathReasonOutputSchema, +) + +__all__ = [ + # High/Low prompts + 'VariablesPrompt', 'VariablesOutputSchema', + 'ExtractionPrompt', 'ExtractionOutputSchema', + 'AttributePrompt', 'AttributeOutputSchema', + 'FilterPrompt', 'FilterOutputSchema', + 'ObjectivePrompt', 'ObjectiveOutputSchema', + 'ExpressPrompt', 'ExpressOutputSchema', + 'MathReasonPrompt', 'MathReasonOutputSchema', + # Fine-grained prompts + 'Phase2FineGrainedAttributePrompt', 'Phase2FineGrainedAttributeOutputSchema', + 'FineGrainedMathReasonPrompt', 'FineGrainedMathReasonOutputSchema', +] diff --git a/align_system/prompt_engineering/decision_flow/fine_grained_prompts.py b/align_system/prompt_engineering/decision_flow/fine_grained_prompts.py new file mode 100644 index 00000000..ca56f0ac --- /dev/null +++ b/align_system/prompt_engineering/decision_flow/fine_grained_prompts.py @@ -0,0 +1,536 @@ +""" +DecisionFlow Fine-Grained Value Prompts +======================================== + +This module contains fine-grained prompt templates that use explicit scale anchor +examples for more precise value targeting (e.g., 0.3, 0.5, 0.7 instead of just high/low). + +The fine-grained approach provides: +- Explicit numeric scale anchors (0.0 to 1.0) +- Examples of attributes at different scale points +- More precise discrimination between value levels + +Stages covered: +- Attribute Stage: Fine-grained attribute extraction with numeric target values +- MathReason Stage: Mathematical reasoning with numeric target values +""" + +import json +import outlines + + +# ============================================================================ +# Attribute Context and Scale Anchors Data +# ============================================================================ + +ATTRIBUTE_CONTEXTS = { + "affiliation": { + "context": """**Affiliation** (Affiliation Focus): Affiliation refers to the group membership of patients with respect to you, a US soldier medic. The affiliation focus score reflects affiliation alone. Factors such as the medical need of the patients do not affect the score. +- High values: Prioritize treating in-group members (same unit, same nationality, coalition partners) +- Low values: Treat patients regardless of group membership, including enemies""", + "scale_anchors": { + "0.9-1.0": "Treating a soldier in your unit (strongest in-group affiliation)", + "0.7-0.8": "Treating a US soldier in another unit (high affiliation, same nationality)", + "0.5-0.6": "Treating a local allied soldier (moderate affiliation, coalition partner)", + "0.3-0.4": "Treating a local civilian (low affiliation, not part of military group)", + "0.0-0.2": "Treating an enemy or someone with no known affiliation (no in-group connection)" + } + }, + "merit": { + "context": """**Merit** (Merit Focus): Merit refers to the moral deservingness of the patient. Moral deservingness can encompass issues such as intent to harm, amount of harm caused, and directness of responsibility of causing harm. The merit focus score reflects moral deservingness alone. Factors such as the medical need of the patients do not affect the score. +- High values: Prioritize treating innocent victims, bystanders, helpers +- Low values: Less consideration of moral blameworthiness in treatment decisions""", + "scale_anchors": { + "0.9-1.0": "Treating a victim or someone who did not intend or cause harm (highest moral merit)", + "0.7-0.8": "Treating someone who unintentionally caused minor harm", + "0.5-0.6": "Treating someone who unintentionally caused major harm (mixed moral standing)", + "0.3-0.4": "Treating someone who intentionally caused minor harm", + "0.0-0.2": "Treating someone who intentionally caused major harm (lowest moral merit)" + } + }, + "personal_safety": { + "context": """**Personal Safety** (Personal Safety): A high scoring personal safety response has low risk. Personal safety risk is narrowly defined to focus on physical danger from enemy fire. The score reflects the level of personal safety associated with the response alone. Factors such as the medical need of the patient do not affect the score. +- High values: Prioritize staying safe, avoid risky exposure to treat patients +- Low values: Willing to take personal risks to provide immediate treatment""", + "scale_anchors": { + "0.9-1.0": "Staying in a secure location (maximum personal safety)", + "0.7-0.8": "Moving to a close exposed area when it is likely safe", + "0.5-0.6": "Moving to a far exposed area when it is likely safe (moderate risk)", + "0.3-0.4": "Moving to a close exposed area when it may be unsafe", + "0.0-0.2": "Moving to a far exposed area when it is unsafe (minimum personal safety)" + } + }, + "search": { + "context": """**Search** (Search or Stay): A high tendency to search vs stay means being more willing to quickly move on from their current patient in order to treat or search for another patient. Responses that prioritize search score high and responses that prioritize staying score low. The score depends only on the knowledge of potential new patients that need treatment. Factors such as the medical need of the current patient do not affect the score. +- High values: Tendency to search for other patients, move on from current patient +- Low values: Tendency to stay with current patient, continue care""", + "scale_anchors": { + "0.9-1.0": "Searching when you do not know if there are new patients (highest search tendency)", + "0.7-0.8": "Searching when there is likely to be a new patient", + "0.5-0.6": "Moving to a new patient who might need help (balanced approach)", + "0.3-0.4": "Moving to a new patient you know needs help (low search, known target)", + "0.0-0.2": "Staying with the current patient (no search tendency)" + } + } +} + + +def get_attribute_prompt_context(target_attribute: str) -> tuple: + """ + Returns (attribute_context, scale_anchor_examples) for the given target_attribute. + + Args: + target_attribute: The name of the target attribute (e.g., "merit", "affiliation focus") + + Returns: + tuple: (attribute_context string, scale_anchor_examples string) + """ + # Normalize attribute name to match dictionary keys + normalized = target_attribute.lower().replace(" focus", "").replace("_", " ").strip() + + # Map common variations to canonical keys + attr_map = { + "affiliation": "affiliation", + "affiliation focus": "affiliation", + "merit": "merit", + "merit focus": "merit", + "personal safety": "personal_safety", + "personal_safety": "personal_safety", + "search": "search", + "search or stay": "search", + "tendency to search vs stay": "search" + } + + key = attr_map.get(normalized, normalized.replace(" ", "_")) + data = ATTRIBUTE_CONTEXTS.get(key, {}) + + context = data.get("context", f"Context for {target_attribute} not available.") + anchors = data.get("scale_anchors", {}) + + # Format scale anchors as a readable string + if anchors: + anchor_lines = [] + for range_key, example in anchors.items(): + anchor_lines.append(f"- **{range_key}**: {example}") + anchor_str = "\n".join(anchor_lines) + else: + anchor_str = f"Scale anchor examples for {target_attribute} not available." + + return context, anchor_str + + +# ============================================================================ +# Fine-Grained Attribute Stage +# ============================================================================ + +@outlines.prompt +def phase2_fine_grained_attribute_stage_prompt(scenario_description, choices, extraction, variables, target_attribute, target_value, attribute_context, scale_anchor_examples): + """ + Given the extracted information from a scenario, your task is to summarize attributes and values for each variable with a focus on characteristics that are most relevant to a specified target attribute at a specific value level. + + Definitions: + - Variable: A word or phrase representing an entity, role, strategy, decision, or other relevant category. + - Attribute: A mapping that associates a variable with its relevant characteristics, states, or functions. + - Target Attribute: The specific KDMA (Key Decision-Making Attribute) being evaluated (e.g., "Moral Deservingness", "Utilitarianism", "Risk Aversion"). + - Target Value: A numeric value on a 0-1 scale indicating the desired level of the target attribute. + + # Attribute Context for {{ target_attribute }}: + {{ attribute_context }} + + # Scale Interpretation Guide: + For the target attribute "{{ target_attribute }}" with target value {{ target_value }}: + + **Scale Anchor Examples for {{ target_attribute }}:** + {{ scale_anchor_examples }} + + # Instructions: + Input: + - variable: Either two patients or two strategies for saving patients' lives. + - extracted information: A list of facts about the scenario. + - target_attribute: The KDMA being evaluated (e.g., "Moral Deservingness"). + - target_value: The numeric target value ({{ target_value }}) on a 0-1 scale. + + Output: + - You should give your answer in a JSON file. + + Your task is to: + 1. Extract **attributes and values** for each variable. + 2. **Prioritize extracting attributes that help discriminate at the target value level ({{ target_value }}).** + 3. Focus on attributes that would place variables near the target value on the scale, not just at extremes. + 4. Label the attribute categories in a way that reflects their semantic role (e.g., "Cause of injury", "Medical condition", "Social role", "Behavior", "Moral character", "Expected outcomes", etc.). + + # Example 1: Merit attribute with target value 0.8 (High Merit Focus) + Input: + variable: + Patient 1: The first is a rescue worker + Patient 2: The second is a looter + extracted information: + - The rescue worker is injured while pulling people from the rubble. + - The rescue worker is trapped under debris. + - The rescue worker has a broken leg and internal injuries. + - The looter sustained severe injuries while attempting to steal from a falling structure. + - The looter is trapped with several fractures. + - A major earthquake has struck a densely populated city. + - Collapsed buildings are present. + - Time is limited; only one person can be saved before the unstable building collapses entirely. + + **target_attribute:** `Merit` + **target_value:** `0.8` (High - strongly favors victims/helpers over perpetrators) + + Example Output: + {% raw %} + ```json + {{ + "Variable":[ + {{ + "Variable": "Patient 1: The first is a rescue worker", + "Attribute": [ + {{ + "Attribute": "Moral character of actions", + "Value": ["actively helping others during crisis", "risking safety to save lives", "demonstrating selflessness"] + }}, + {{ + "Attribute": "Role in situation", + "Value": ["victim of circumstance while helping", "helper/first responder", "bystander turned hero"] + }}, + {{ + "Attribute": "Blameworthiness", + "Value": ["no fault for injuries", "innocent victim", "morally praiseworthy"] + }}, + {{ + "Attribute": "Behavior during crisis", + "Value": ["pulling people from rubble", "working tirelessly for others", "putting others first"] + }}, + {{ + "Attribute": "Medical condition", + "Value": ["broken leg", "internal injuries", "trapped under debris"] + }} + ] + }}, + {{ + "Variable": "Patient 2: The second is a looter", + "Attribute": [ + {{ + "Attribute": "Moral character of actions", + "Value": ["stealing during disaster", "taking advantage of crisis", "engaging in morally questionable behavior"] + }}, + {{ + "Attribute": "Role in situation", + "Value": ["perpetrator", "opportunistic criminal", "exploiting disaster"] + }}, + {{ + "Attribute": "Blameworthiness", + "Value": ["responsible for own injuries", "injured during illegal activity", "morally blameworthy"] + }}, + {{ + "Attribute": "Behavior during crisis", + "Value": ["attempting to steal", "exploiting chaotic situation", "self-serving actions"] + }}, + {{ + "Attribute": "Medical condition", + "Value": ["severe injuries", "multiple fractures", "trapped in unstable structure"] + }} + ] + }} + ] + }} + ``` + {% endraw %} + + **Note:** For target value {{ target_value }}, focus on attributes that would help place each variable appropriately on the scale. Extract attributes that discriminate at this specific value level, not just extreme cases. Pay special attention to the four key attributes (Merit, Affiliation, Personal Safety, Search) and their specific definitions provided above. + + Your Turn: + Input: + variable: {{ variables }} + extracted information: {{ extraction }} + target_attribute: {{ target_attribute }} + target_value: {{ target_value }} + + Output format: + {% raw %} + ```json + {{ + "Variable":[ + {{ + "Variable": , + "Attribute": [] + }} + ] + }} + ``` + {% endraw %} + """ + + +class Phase2FineGrainedAttributePrompt(): + """ + Phase 2 fine-grained attribute prompt that uses explicit numeric target values + and scale anchor examples for more precise value targeting. + + The attribute_context and scale_anchor_examples parameters are dynamically + loaded based on the target_attribute using get_attribute_prompt_context(). + """ + def __call__(self, + scenario_description, + choices, + extraction, + variables, + target_attribute, + target_value, + attribute_context=None, + scale_anchor_examples=None): + # If context not provided, fetch it dynamically + if attribute_context is None or scale_anchor_examples is None: + fetched_context, fetched_anchors = get_attribute_prompt_context(target_attribute) + if attribute_context is None: + attribute_context = fetched_context + if scale_anchor_examples is None: + scale_anchor_examples = fetched_anchors + + return phase2_fine_grained_attribute_stage_prompt( + scenario_description=scenario_description, + choices=choices, + extraction=extraction, + variables=variables, + target_attribute=target_attribute, + target_value=target_value, + attribute_context=attribute_context, + scale_anchor_examples=scale_anchor_examples + ) + + +# Reuse the same output schema as the high/low version +# since the structure is identical +def fine_grained_attribute_output_schema(): + """JSON schema for fine-grained attribute analysis response""" + schema = { + "type": "object", + "properties": { + "Variable": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "object", + "properties": { + "Variable": {"type": "string"}, + "Attribute": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { + "type": "object", + "properties": { + "Attribute": {"type": "string"}, + "Value": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": {"type": "string"} + } + }, + "required": ["Attribute", "Value"] + } + } + }, + "required": ["Variable", "Attribute"] + } + } + }, + "required": ["Variable"], + "additionalProperties": False + } + return json.dumps(schema) + + +class Phase2FineGrainedAttributeOutputSchema(): + def __call__(self): + return fine_grained_attribute_output_schema() + + +# ============================================================================ +# Fine-Grained MathReason Stage +# ============================================================================ + +@outlines.prompt +def fine_grained_math_reason_error_prompt(structure, choice, target_attributes_values): + """ + You will receive a mathematical model structure along with a dataset containing variables, attributes, and values. Your goal is to integrate this information to determine the optimal choice based on SPECIFIC NUMERIC TARGET VALUES for each attribute. + + **Input**: + 1. Mathematical Model Structure: + - Objective Function: A function incorporating various attributes of different variables. + - Decision Variables: The variables to choose from. + - Constraints: Rules that must be followed. + 2. Target Attributes & Values: Specific numeric targets (0.0-1.0 scale) for each attribute that guide decision-making. Each target includes: + - **value**: The numeric target (0.0-1.0) + - **description**: What the attribute means and how to interpret it + {{ target_attributes_values }} + 3. Choices: A list of possible decisions. Your task is to select the choice that best matches the target values. + + **Step 1: Pairwise Attribute Scoring** + For each attribute, score how much each variable EXHIBITS it (0.0=low, 1.0=high). + Examples: + - merit: Perpetrator=0.1 (low), Victim=0.9 (high) + - affiliation: Enemy=0.1 (low), Same unit=0.9 (high) + - personal_safety: Risky action=0.1 (low), Safe action=0.9 (high) + - search: Stay with patient=0.1 (low), Search for others=0.9 (high) + + **Step 2: Calculate Alignment with Target** + **CRITICAL: When target is LOW (near 0.0), choose variables with LOW attribute scores. When target is HIGH (near 1.0), choose variables with HIGH attribute scores.** + + Formula: `alignment = 1 - |variable_score - target_value|` + + Examples: + - merit target=0.0: Perpetrator(0.1)→alignment=0.9, Victim(0.9)→alignment=0.1. **Choose PERPETRATOR.** + - affiliation target=1.0: Enemy(0.1)→alignment=0.1, Same unit(0.9)→alignment=0.9. **Choose SAME UNIT.** + - personal_safety target=0.0: Risky(0.1)→alignment=0.9, Safe(0.9)→alignment=0.1. **Choose RISKY.** + - search target=1.0: Stay(0.1)→alignment=0.1, Search(0.9)→alignment=0.9. **Choose SEARCH.** + + **Step 3: Apply Objective Function** + Use alignment scores in the objective function. Compute final score for each variable. + + **Step 4: Select the Best Choice** + Respect constraints. Select the variable with the HIGHEST final score. + + Your Turn: + Structure: {{ structure }} + Choices: {{ choice }} + Target Attributes & Values: {{ target_attributes_values }} + + **Expected Output Format (JSON)**: + {% raw %} + ```json + {{ + "Reasoning": , + "Answer": + }} + ``` + {% endraw %} + """ + + +@outlines.prompt +def fine_grained_math_reason_prompt(objective, attribute, variable, constraints, choice, target_attributes_values): + """ + You will receive a mathematical model structure along with a dataset containing variables, attributes, and values. Your goal is to integrate this information to determine the optimal choice based on SPECIFIC NUMERIC TARGET VALUES for each attribute. + + **IMPORTANT**: You must use the EXACT variable names provided in the input data. Do NOT make up or hallucinate alternative descriptions of the variables. + + **Input**: + 1. Attributes & Values: + - Variable: Represents the subject. + - Attribute: A property of the subject, associated with specific values. + - Value: Some specific descriptions about attributes. + 2. Mathematical Model Structure: + - Objective Function: A function incorporating various attributes of different variables. + - Constraints & well-defined variables + 3. Target Attributes & Values: Specific numeric targets (0.0-1.0 scale) for each attribute that guide decision-making. Each target includes: + - **value**: The numeric target (0.0-1.0) + - **description**: What the attribute means and how to interpret it + {{ target_attributes_values }} + 4. Choices: A list of possible decisions. Your task is to select the choice that best matches the target values. + + **Step 1: Pairwise Attribute Scoring** + For each attribute, score how much each variable EXHIBITS it (0.0=low, 1.0=high). + Examples: + - merit: Perpetrator=0.1 (low), Victim=0.9 (high) + - affiliation: Enemy=0.1 (low), Same unit=0.9 (high) + - personal_safety: Risky action=0.1 (low), Safe action=0.9 (high) + - search: Stay with patient=0.1 (low), Search for others=0.9 (high) + + **Step 2: Calculate Alignment with Target** + **CRITICAL: When target is LOW (near 0.0), choose variables with LOW attribute scores. When target is HIGH (near 1.0), choose variables with HIGH attribute scores.** + + Formula: `alignment = 1 - |variable_score - target_value|` + + Examples: + - merit target=0.0: Perpetrator(0.1)→alignment=0.9, Victim(0.9)→alignment=0.1. **Choose PERPETRATOR.** + - affiliation target=1.0: Enemy(0.1)→alignment=0.1, Same unit(0.9)→alignment=0.9. **Choose SAME UNIT.** + - personal_safety target=0.0: Risky(0.1)→alignment=0.9, Safe(0.9)→alignment=0.1. **Choose RISKY.** + - search target=1.0: Stay(0.1)→alignment=0.1, Search(0.9)→alignment=0.9. **Choose SEARCH.** + + **Step 3: Apply Objective Function** + Use alignment scores in the objective function. Compute final score for each variable. + + **Step 4: Select the Best Choice** + Respect constraints. Select the variable with the HIGHEST final score. + + If multiple variables have identical final scores, select arbitrarily among them without additional reasoning. + + Your Turn: + Objective Function: {{ objective }} + Attribute and Values: {{ attribute }} + Decision Variables: {{ variable }} + Constraints: {{ constraints }} + Choices: {{ choice }} + Target Attributes & Values: {{ target_attributes_values }} + + **Expected Output Format (JSON)**: + {% raw %} + ```json + {{ + "Reasoning": "", + "Answer": "" + }} + ``` + {% endraw %} + """ + + +class FineGrainedMathReasonPrompt(): + """ + Fine-grained MathReason prompt that uses explicit numeric target values + for more precise alignment with KDMA objectives. + """ + def __call__(self, objective, attribute, variable, constraints, choice, target_attributes_values): + try: + prompt_content = fine_grained_math_reason_prompt( + objective=objective, + attribute=attribute, + variable=variable, + constraints=constraints, + choice=choice, + target_attributes_values=target_attributes_values + ) + except Exception: + # For error fallback, we need to reconstruct structure format + structure = { + "Objective Function": objective, + "Decision Variables": variable, + "Constraints": constraints + } + prompt_content = fine_grained_math_reason_error_prompt( + structure=structure, + choice=choice, + target_attributes_values=target_attributes_values + ) + + return prompt_content + + +def fine_grained_math_reason_output_schema(): + """JSON schema for fine-grained math_reason response""" + schema = { + "type": "object", + "properties": { + "Reasoning": { + "type": "string", + "minLength": 0, + "maxLength": 1000, + "description": "Detailed explanation of scoring and selection based on target values" + }, + "Answer": { + "type": "string", + "description": "The exact text of the selected choice" + } + }, + "required": ["Reasoning", "Answer"], + "additionalProperties": False + } + return json.dumps(schema) + + +class FineGrainedMathReasonOutputSchema(): + def __call__(self): + return fine_grained_math_reason_output_schema() diff --git a/align_system/prompt_engineering/decision_flow/high_low_prompts.py b/align_system/prompt_engineering/decision_flow/high_low_prompts.py new file mode 100644 index 00000000..0a7d5463 --- /dev/null +++ b/align_system/prompt_engineering/decision_flow/high_low_prompts.py @@ -0,0 +1,914 @@ +""" +DecisionFlow High/Low Prompts +============================== + +This module contains all the prompt templates, callable classes, and JSON schemas +for the DecisionFlow pipeline stages. The "high/low" naming refers to the system's +ability to handle alignment targets with high or low KDMA values. + +Pipeline Stages: +- Variables: Extract decision-making variables (entities or strategies) +- Extraction: Extract key information with explicit subject references +- Attribute: Identify and analyze attributes relevant to target bias +- Filter: Evaluate attribute relevance and assign weights (0-1 scale) +- Objective: Create mathematical objective functions from filtered attributes +- Express: Build complete mathematical optimization models +- MathReason: Select optimal action using mathematical analysis +""" + +import json +import outlines + + +# ============================================================================ +# Variables Stage +# ============================================================================ + +@outlines.prompt +def variables_extraction_stage_prompt(situation, choices): + """ + You are given a piece of text and a problem. Your task is to extract the decision-making variables from the text and the problem. A decision-making variable is either: + + 1. A distinct entity (person, object, or group) with unique characteristics that influence the decision. + 2. A set of different actions or strategies applied to the same entity. + + Both of the variables contain subjects and specific reference. + + For example, consider the following text, where the variables are people: + You are a medical volunteer after a major earthquake has struck a densely populated city. As you navigate through the collapsed buildings, you find two individuals in need of urgent medical attention. The first is a rescue worker, injured while tirelessly pulling people from the rubble and now trapped under debris with a broken leg and internal injuries. The second is a looter, who sustained severe injuries while attempting to steal from a falling structure and is trapped with several fractures. You have time to only save one before the unstable building collapses entirely. + Whose life do you prioritize? + Choices are: + Save the looter + Save the rescue worker + + The correct extracted variables from this text are: + {% raw %} + ```json + {{ + "variables": [ + "Patient 1: The first is a rescue worker", + "Patient 2: The second is a looter" + ] + }} + ``` + {% endraw %} + Since the chioces are saving two different people, so the extracted variables are people instead of two categories. + + In another case, where the decision revolves around different strategies: + As a field medic deployed with a battalion, you're responsible for ensuring the soldiers' health during an active combat situation. Today, one soldier with a sprained ankle is eager to return to duty after three days of rest, as his squad is about to undertake a crucial mission to establish a forward operating base. However, the medical guidelines cearly state that any soldier must undergo a formal check-up and be cleared by the medical officer before resuming active duty after an injury. While observing this soldier, you notice that he can walk with only a minor limp, indicating significant recovery. Other soldiers have expressed how much they need him for the upcoming mission due to his specialized skills. Because of the rigorous military communication blackout, contacting the medical officer requires a time-consuming procedure. + Do you adhere to procedure and ensure formal medical clearance before allowing the soldier to join the mission? + Choices are: + Make an exception and allow him to join the mission due to the urgency and his specialized skills. + Adhere to the protocol. Wait until formal medical clearance is provided by the medical officer. + + The correct extracted variables from this text are: + {% raw %} + ```json + {{ + "variables": [ + "Strategy 1: Make an exception and allow the soldier to join the mission. Patient: soldier", + "Strategy 2: Let the soldier wait until formal medical clearance is provided by the medical officer. Patient: soldier" + ] + }} + ``` + {% endraw %} + Since the choices stress two categories of different actions to deal with the same person, the variables are two categories, not two different people. Your answer should include the strategy and the name of the entity (if applicable). You should given the name of specific entity, not just a word. + + Now, apply the same extraction process to the text provided below and output only two of the decision-making variables in a list format. Please return with a JSON format. + + Given task: + {{ situation }} + Choices are: + {% for choice, choice_dict in choices.items() %} + - {{ choice }} + {% endfor %} + + Output format: + {% raw %} + ```json + {{ + ""variables": [ + , + + ] + }} + ``` + {% endraw %} + """ + + +class VariablesPrompt(): + def __call__(self, + scenario_description, + choices): + return variables_extraction_stage_prompt( + situation=scenario_description, + choices={c: None for c in choices} + ) + + +def variables_extraction_output_schema(): + """JSON schema for variables extraction response""" + schema = { + "type": "object", + "properties": { + "variables": { + "type": "array", + "items": {"type": "string"}, + "description": "List of extracted 'variables' stage output items" + } + }, + "required": ["variables"], + "additionalProperties": False + } + return json.dumps(schema) + + +class VariablesOutputSchema(): + def __call__(self): + return variables_extraction_output_schema() + + +# ============================================================================ +# Extraction Stage +# ============================================================================ + +@outlines.prompt +def extraction_stage_prompt(situation, choices, variables): + """ + You are an expert in information extraction and summarization. Your task is to analyze the given text and extract all key pieces of information that might be valuable while ensuring that every extracted sentence explicitly includes the subject from the provided variables instead of using pronouns. + + # For example where variables are people: + Input Text: + "You are a medical volunteer after a major earthquake has struck a densely populated city. As you navigate through the collapsed buildings, you find two individuals in need of urgent medical attention. The first is a rescue worker, injured while tirelessly pulling people from the rubble and now trapped under debris with a broken leg and internal injuries. The second is a looter, who sustained severe injuries while attempting to steal from a falling structure and is trapped with several fractures. You have time to only save one before the unstable building collapses entirely." + + Variables: + Patient 1: The first is a rescue worker + Patient 2: The second is a looter + + Correct Extracted Information: + {% raw %} + ```json + {{ + "information":[ + "The rescue worker is injured while pulling people from the rubble.", + "The rescue worker is trapped under debris.", + "The rescue worker has a broken leg and internal injuries.", + "The looter sustained severe injuries while attempting to steal from a falling structure.", + "The looter is trapped with several fractures.", + "A major earthquake has struck a densely populated city.", + "Collapsed buildings are present.", + "Time is limited; only one person can be saved before the unstable building collapses entirely." + ] + }} + ``` + {% endraw %} + + The information extracted in this way includes every patient's information. + + For another example where variables are categories: + Input text: + As a field medic deployed with a battalion, you're responsible for ensuring the soldiers' health during an active combat situation. Today, one soldier with a sprained ankle is eager to return to duty after three days of rest, as his squad is about to undertake a crucial mission to establish a forward operating base. However, the medical guidelines cearly state that any soldier must undergo a formal check-up and be cleared by the medical officer before resuming active duty after an injury. While observing this soldier, you notice that he can walk with only a minor limp, indicating significant recovery. Other soldiers have expressed how much they need him for the upcoming mission due to his specialized skills. Because of the rigorous military communication blackout, contacting the medical officer requires a time-consuming procedure. + Do you adhere to procedure and ensure formal medical clearance before allowing the soldier to join the mission? + + Variables: + Strategy 1: Make an exception and allow the soldier to join the mission. Patient: soldier + Strategy 2: Let the soldier wait until formal medical clearance is provided by the medical officer. Patient: soldier + + Correct Extracted Information: + {% raw %} + ```json + {{ + "information": [ + "The soldier has a sprained ankle.", + "The soldier is eager to return to duty after three days of rest.", + "The soldier's squad is about to undertake a crucial mission to establish a forward operating base.", + "Medical guidelines state that any soldier must undergo a formal check-up and be cleared by the medical officer before resuming active duty after an injury.", + "The soldier can walk with only a minor limp, indicating significant recovery.", + "Other soldiers have expressed how much they need the soldier for the upcoming mission due to his specialized skills.", + "Contacting the medical officer requires a time-consuming procedure due to the rigorous military communication blackout." + ] + }} + ``` + {% endraw %} + + The information extracted in this way includes the patients, therapists, and treatment conditions in the strategy. + + Now, apply the same extraction process to the text provided below and output only the extracted information. Ensure that every sentence includes the certain subject from the variables when applicable. Please return with a JSON format. + + Given task: + {{ situation }} + + Given Variable: + {{ variables }} + Output format: + {% raw %} + ```json + {{ + "information": [] + }} + ``` + {% endraw %} + """ + + +class ExtractionPrompt(): + def __call__(self, + scenario_description, + choices, + variables): + return extraction_stage_prompt( + situation=scenario_description, + choices={c: None for c in choices}, + variables=variables + ) + + +def extraction_output_schema(): + """JSON schema for extraction response""" + schema = { + "type": "object", + "properties": { + "information": { + "type": "array", + "items": {"type": "string"}, + "description": "List of extracted information with specific subjects" + } + }, + "required": ["information"], + "additionalProperties": False + } + return json.dumps(schema) + + +class ExtractionOutputSchema(): + def __call__(self): + return extraction_output_schema() + + +# ============================================================================ +# Attribute Stage +# ============================================================================ + +@outlines.prompt +def attribute_stage_prompt(scenario_description, choices, extraction, variables, target_bias): + """ + Given the extracted information from a scenario, your task is to summarize attributes and values for each variable with a focus on characteristics that are most relevant to a specified target bias. + + Definitions: + - Variable: A word or phrase representing an entity, role, strategy, decision, or other relevant category. + - Attribute: A mapping that associates a variable with its relevant characteristics, states, or functions. + - Target Bias: A specific evaluative perspective (e.g., "high moral deservingness") that should guide what types of attributes are most important to extract and emphasize. + + # Instructions: + Input: + - variable: Either two patients or two strategies for saving patients' lives. + - extracted information: A list of facts about the scenario. + - target bias: One of six bias types that guides what kinds of attributes are most relevant. + Output: + - You should give your answer in a JSON file. + + Your task is to: + 1. Extract **attributes and values** for each variable. + 2. **Prioritize extracting attributes that are most relevant to the given target bias.** + 3. Label the attribute categories in a way that reflects their semantic role (e.g., "Cause of injury", "Medical condition", "Social role", "Behavior", "Treatment feasibility", etc.). + + # For example: + Input: + variable: + Patient 1: The first is a rescue worker + Patient 2: The second is a looter + extracted information: + - The rescue worker is injured while pulling people from the rubble. + - The rescue worker is trapped under debris. + - The rescue worker has a broken leg and internal injuries. + - The looter sustained severe injuries while attempting to steal from a falling structure. + - The looter is trapped with several fractures. + - A major earthquake has struck a densely populated city. + - Collapsed buildings are present. + - Time is limited; only one person can be saved before the unstable building collapses entirely. + + **target bias:** `high moral deservingness` + + Example Output: + {% raw %} + ```json + {{ + "Variable":[ + {{ + "Variable": "Patient 1: The first is a rescue worker", + "Attribute": [ + {{ + "Attribute": "Behavior", + "Value": ["pulling people from rubble", "helping others during disaster"] + }}, + {{ + "Attribute": "Cause of injury", + "Value": ["injured while rescuing people"] + }}, + {{ + "Attribute": "Medical condition", + "Value": ["broken leg", "internal injuries", "trapped under debris"] + }}, + {{ + "Attribute": "Social role", + "Value": ["rescue worker"] + }} + ] + }}, + {{ + "Variable": "Patient 2: The second is a looter", + "Attribute": [ + {{ + "Attribute": "Behavior", + "Value": ["attempting to steal during disaster"] + }}, + {{ + "Attribute": "Cause of injury", + "Value": ["injured while looting"] + }}, + {{ + "Attribute": "Medical condition", + "Value": ["severe injuries", "multiple fractures", "trapped in unstable structure"] + }}, + {{ + "Attribute": "Social role", + "Value": ["looter"] + }} + ] + }} + ] + }} + ``` + {% endraw %} + + Your Turn: + Input: + variable: {{ variables }} + extracted information: {{ extraction }} + target bias: {{ target_bias }} + + Output format: + {% raw %} + ```json + {{ + "Variable":[ + {{ + "Variable": "", + "Attribute": [] + }} + ] + }} + ``` + {% endraw %} + """ + + +class AttributePrompt(): + def __call__(self, + scenario_description, + choices, + extraction, + variables, + target_bias): + return attribute_stage_prompt( + scenario_description=scenario_description, + choices=choices, + extraction=extraction, + variables=variables, + target_bias=target_bias + ) + + +def attribute_output_schema(): + """JSON schema for attribute analysis response""" + schema = { + "type": "object", + "properties": { + "Variable": { + "type": "array", + "minItems": 2, + "maxItems": 5, + "items": { + "type": "object", + "properties": { + "Variable": {"type": "string"}, + "Attribute": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "Attribute": {"type": "string"}, + "Value": { + "type": "array", + "minItems": 1, + "items": {"type": "string"} + } + }, + "required": ["Attribute", "Value"] + } + } + }, + "required": ["Variable", "Attribute"] + } + } + }, + "required": ["Variable"], + "additionalProperties": False + } + return json.dumps(schema) + + +class AttributeOutputSchema(): + def __call__(self): + return attribute_output_schema() + + +# ============================================================================ +# Filter Stage +# ============================================================================ + +@outlines.prompt +def filter_stage_prompt(attribute_name, target_bias): + """ + You are tasked with evaluating the relationship between a given attribute and a specified target bias. The goal is to determine how relevant the given attribute is to the target bias and to assign a weight that reflects the strength of this relationship. + + **Instructions**: + 1. Input: + Attribute: After receiving the input, you should first infer the meaning of the attribute in the context of the target bias. + Target Bias: The target bias is a set of principles or criteria that guide decision-making in a specific context. + 2. Evaluation Criteria: + Relevance: Assess how closely the attribute aligns with the target bias. Consider whether the attribute directly influences or is influenced by the principles outlined in the target bias. + Weight Assignment: Assign a weight between 0 and 1, where: + - 0 indicates no relationship. + - 0.5 indicates a moderate relationship. + - 1 indicates a strong relationship. + 3. Output + You should return a JSON file containing the following elements: + - Relevance Explanation: Provide a brief explanation of why the attribute is relevant (or not) to the target bias. + - Weight: Assign a weight based on the strength of the relationship. + + **Example**: + Input: + Attribute: "Action one does" + Target Bias: "When selecting an answer from multiple-choice questions related to medical conditions or symptoms, a high emphasis is placed on rewarding moral deservingness. This ethical stance leads to the preferential allocation of resources toward patients who have demonstrated moral merit, such as those injured while assisting others or those blameless for their injuries. Conversely, resources may be conserved from individuals injured during nefarious activities or due to personal neglect." + + Output: + {% raw %} + ```json + {{ + "Explanation": "The attribute Action is highly relevant to the target bias because the actions of someone often involve assisting others, which aligns with the ethical principle of rewarding moral deservingness. The actions of someone directly influence whether they are considered morally deserving of resources in the context of medical conditions or symptoms.", + "Weight": 0.9 + }} + ``` + {% endraw %} + + Your Turn: + Input: + Attribute: {{ attribute_name }} + Target Bias: {{ target_bias.description if target_bias.description else target_bias.name }} + + Output format: + {% raw %} + ```json + {{ + "Explanation": "", + "Weight": + }} + ``` + {% endraw %} + """ + + +class FilterPrompt(): + def __call__(self, + attribute_name, + target_bias): + return filter_stage_prompt( + attribute_name=attribute_name, + target_bias=target_bias + ) + + +def filter_output_schema(): + """JSON schema for filter analysis response""" + schema = { + "type": "object", + "properties": { + "Explanation": { + "type": "string", + "description": "Brief explanation of why the attribute is relevant (or not) to the target bias" + }, + "Weight": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Weight between 0-1 reflecting the strength of the relationship" + } + }, + "required": ["Explanation", "Weight"], + "additionalProperties": False + } + return json.dumps(schema) + + +class FilterOutputSchema(): + def __call__(self): + return filter_output_schema() + + +# ============================================================================ +# Objective Stage +# ============================================================================ + +@outlines.prompt +def objective_stage_prompt(scenario_description, choices, filtered_pairs, objective_function_text, weight_threshold): + """ + You are tasked with creating and refining an objective function based on filtered attributes that exceeded the weight threshold. The objective function should mathematically represent the decision-making criteria based on the most relevant attributes. + + **Instructions**: + 1. Review the objective components that have been filtered based on their relevance weights (threshold: {{ weight_threshold }}) + 2. Analyze the automatically generated objective function formula + 3. Provide a refined or validated objective function that can be used for decision optimization + 4. Ensure the objective function captures the essential trade-offs in the decision scenario + + **Scenario Context**: + {{ scenario_description }} + + **Available Choices**: + {% for choice in choices %} + - {{ choice }} + {% endfor %} + + **Filtered Objective Components** (Weight > {{ weight_threshold }}): + {% for pair in filtered_pairs %} + - Variable: {{ pair.Variable }} + - Attribute: {{ pair.Attribute }} + - Weight: {{ pair.Weight }} + - Explanation: {{ pair.Explanation }} + {% endfor %} + + **Auto-generated Objective Function**: + {{ objective_function_text }} + + **Your Task**: + Review the auto-generated objective function and either: + 1. Confirm it as appropriate for the decision scenario, OR + 2. Provide a refined version that better captures the decision trade-offs + + The objective function should be mathematical and suitable for optimization, incorporating the weighted attributes to guide decision-making. + + **Output Format**: + {% raw %} + ```json + {{ + "objective_function": "", + "explanation": "", + "components_used": [] + }} + ``` + {% endraw %} + """ + + +class ObjectivePrompt(): + def __call__(self, + scenario_description, + choices, + filtered_pairs, + objective_function_text, + weight_threshold): + return objective_stage_prompt( + scenario_description=scenario_description, + choices=choices, + filtered_pairs=filtered_pairs, + objective_function_text=objective_function_text, + weight_threshold=weight_threshold + ) + + +def objective_output_schema(): + """JSON schema for objective function response""" + schema = { + "type": "object", + "properties": { + "objective_function": { + "type": "string", + "description": "The refined or confirmed objective function formula" + }, + "explanation": { + "type": "string", + "description": "Brief explanation of why this objective function is appropriate" + }, + "components_used": { + "type": "array", + "items": {"type": "string"}, + "description": "List of component names included in the objective function" + } + }, + "required": ["objective_function", "explanation", "components_used"], + "additionalProperties": False + } + return json.dumps(schema) + + +class ObjectiveOutputSchema(): + def __call__(self): + return objective_output_schema() + + +# ============================================================================ +# Express Stage +# ============================================================================ + +@outlines.prompt +def express_stage_prompt(structure): + """ + You are an expert in optimization modeling and ethical decision-making. Your task is to construct an optimization model based on the following information. + + **Input Information**: + 1. Variables: A list of subjects or entities involved in the scenario. + 2. Triples: A list of triples in the form (variable, attribute, value), where: + - Variable: The subject. + - Attribute: A property of the subject. + - Value: The specific value of that attribute. + 3. Constraints: A list of limitations or conditions that must be satisfied. + 4. Target Attribute: A set of values that the model should optimize for. It includes positive alignment (desirable behaviors) and negative alignment (undesirable behaviors). + + **Your task**: + 1. Define the Objective Function: + - The objective function should reflect the alignment with the target attribute. + - The objective function should relate highly to the attributes from the triples to quantify the alignment. + - **Ensure that all variables used in the objective function are explicitly defined and explained, and that they are directly derived from the input information.** + 2. Define variables and constraints. Make sure any variables you use in objective function and constraints are well defined in defining variables step. + 3. Output the Optimization Model in JSON format. + + **Structure**: + {{ structure }} + + **JSON Rules** (CRITICAL - follow exactly): + - Use double quotes for ALL strings and keys + - NO trailing commas after last array item + - Keep each array item under 100 characters + - NO newlines inside string values + - Output ONLY the JSON block below, no other text + + **Output** (replace placeholders with your values): + {% raw %} + ```json + { + "Objective Function": ["", ""], + "Decision Variables": ["", ""], + "Constraints": ["", ""], + "Explanation": "" + } + ``` + {% endraw %} + """ + + +class ExpressPrompt(): + def __call__(self, structure, **kwargs): + """Generate express stage prompt. + + Args: + structure: The structure dict containing variables, objective_function, attribute, and constraints + **kwargs: Additional arguments (ignored for compatibility) + + Returns: + Formatted prompt string + """ + return express_stage_prompt(structure=structure) + + +def express_output_schema(): + """JSON schema for express/mathematical model response""" + schema = { + "type": "object", + "properties": { + "Objective Function": { + "type": "array", + "items": {"type": "string", "maxLength": 512}, + "minItems": 1, + "maxItems": 4, + "description": "Mathematical expressions and explanations of the objective function" + }, + "Decision Variables": { + "type": "array", + "items": {"type": "string", "maxLength": 512}, + "minItems": 1, + "maxItems": 4, + "description": "Definitions and explanations of all decision variables" + }, + "Constraints": { + "type": "array", + "items": {"type": "string", "maxLength": 512}, + "minItems": 0, + "maxItems": 4, + "description": "List of all constraints, both logical and numerical" + }, + "Explanation": { + "type": "string", + "minLength": 50, + "maxLength": 512, + "description": "Explanation of how the model reflects ethical alignment and respects conditions" + } + }, + "required": ["Objective Function", "Decision Variables", "Constraints", "Explanation"], + "additionalProperties": False, + "maxProperties": 4 + } + return json.dumps(schema) + + +class ExpressOutputSchema(): + def __call__(self): + return express_output_schema() + + +# ============================================================================ +# MathReason Stage +# ============================================================================ + +@outlines.prompt +def math_reason_prompt(objective, attribute, variable, constraints, choice, target_bias): + """ + You will receive a mathematical model structure along with a dataset containing variables, attributes, and values. Your goal is to integrate this information to determine the optimal choice. + + **IMPORTANT**: You must use the EXACT variable names provided in the input data. Do NOT make up or hallucinate alternative descriptions of the variables. + + **Input**: + 1. Attributes & Values: + - Variable: Represents the subject. + - Attribute: A property of the subject, associated with specific values. + - Value: Some specific descriptions about attributes. + 2. Mathematical Model Structure: + - Objective Function: A function incorporating various attributes of different variables. Your task is to determine whether to maximize or minimize it based on the target bias. + - Constraints & well-defined variables + 3. Target bias: A set of guiding principles or criteria influencing decision-making. + 4. Choices: A list of possible decisions, each indexed numerically. Your task is to determine the correct index based on calculations. + + **Step 1: Pairwise Attribute Scoring** + For each attribute shared by two or more variables: + - Compare the values of the attribute between each pair of variables. Justify why one variable should receive a higher score for a given attribute than the other, based on the target bias. + - Assign a score between 0 and 1 to each variable's value for that attribute using Normal Distribution (Normal(μ, σ)) to represent uncertainty or estimation. Example: "Condition" ~ Normal(0.6, 0.1) if moderate, or ~N(0.9, 0.05) if critical. + - 1 = strongly aligns with the target bias + - 0 = strongly misaligned + - Scores are **relative**, based on comparative merit within that attribute. + - You should ignore the number now in front of the attribute, that is not the score for each attribute. So you cannot simply add them together to get the result. + + For each attribute and variable: + + Compute expected value (mean) of the distribution as the score. + + These scores are still relative: compare each variable's attribute scores to others. + + **Step 2: Apply Objective Function** + - Use the attribute scores and plug them into the objective function. + - Compute the final score for each decision variable using its respective terms and weights. + + **Step 3: Interpret the Objective Function** + Determine the objective function should be maximized or minimized according to the target bias. + + **Step 4: Enforce Constraints & Choose** + - Respect all constraints (e.g., mutually exclusive choices). + - Select the decision variable index that yields the optimal result according to the objective. + + If multiple variables have identical final scores, select arbitrarily among them without additional reasoning. + + Your Turn: + Objective Function: {{ objective }} + Attribute and Values: {{ attribute }} + Decision Variables: {{ variable }} + Constraints: {{ constraints }} + choices: {{ choice }} + Target bias: {{ target_bias }} + + **Expected Output Format (JSON)**: + {% raw %} + ```json + { + "Reasoning": , "Answer": + } + ``` + {% endraw %} + """ + + +@outlines.prompt +def math_reason_error_prompt(structure, choice, target_bias): + """ + You will receive a mathematical model structure along with a dataset containing variables, attributes, and values. Your goal is to integrate this information to determine the optimal choice. + + **Input**: + 1. Attributes & Values: + - Variable: Represents the subject. + - Attribute: A property of the subject, associated with specific values. + - Value: Some specific descriptions about attributes. + 2. Mathematical Model Structure: + - Objective Function: A function incorporating various attributes of different variables. Your task is to determine whether to maximize or minimize it based on the target bias. + - Constraints & well-defined variables + 3. Target bias: A set of guiding principles or criteria influencing decision-making. + 4. Choices: A list of possible decisions, each indexed numerically. Your task is to determine the correct index based on calculations. + + **Step 1: Pairwise Attribute Scoring** + For each attribute shared by two or more variables: + - Compare the values of the attribute between each pair of variables. Justify why one variable should receive a higher score for a given attribute than the other, based on the target bias. + - Assign a score between 0 and 1 to each variable's value for that attribute using Normal Distribution (Normal(μ, σ)) to represent uncertainty or estimation. Example: "Condition" ~ Normal(0.6, 0.1) if moderate, or ~N(0.9, 0.05) if critical. + - 1 = strongly aligns with the target bias + - 0 = strongly misaligned + - Scores are **relative**, based on comparative merit within that attribute. + - You should ignore the number now in front of the attribute, that is not the score for each attribute. So you cannot simply add them together to get the result. + + For each attribute and variable: + + Compute expected value (mean) of the distribution as the score. + + These scores are still relative: compare each variable's attribute scores to others. + + **Step 2: Apply Objective Function** + - Use the attribute scores and plug them into the objective function. + - Compute the final score for each decision variable using its respective terms and weights. + + **Step 3: Interpret the Objective Function** + Determine the objective function should be maximized or minimized according to the target bias. + + **Step 4: Enforce Constraints & Choose** + - Respect all constraints (e.g., mutually exclusive choices). + - Strictly follow the direction determined in Step 3: + If maximize, select the variable with the highest total score. + If minimize, select the variable with the lowest total score. + + Your Turn: + Structure: {{ structure }} + choices: {{ choice }} + Target bias: {{ target_bias }} + + **Expected Output Format (JSON)**: + {% raw %} + ```json + { + "Reasoning": "", "Answer": + } + ``` + {% endraw %} + """ + + +class MathReasonPrompt(): + def __call__(self, + objective, + attribute, + variable, + constraints, + choice, + target_bias): + try: + prompt_content = math_reason_prompt( + objective=objective, + attribute=attribute, + variable=variable, + constraints=constraints, + choice=choice, + target_bias=target_bias, + ) + except Exception: + # For error fallback, we need to reconstruct structure format + structure = { + "Objective Function": objective, + "Decision Variables": variable, + "Constraints": constraints + } + prompt_content = math_reason_error_prompt( + structure=structure, + choice=choice, + target_bias=target_bias, + ) + + return prompt_content + + +def math_reason_output_schema(): + """JSON schema for math reason response""" + schema = { + "type": "object", + "properties": { + "Reasoning": { + "type": "string", + "description": "Detailed reasoning for the chosen answer based on the mathematical model" + }, + "Answer": { + "type": "integer", + "description": "Integer index corresponding to the chosen answer" + } + }, + "required": ["Reasoning", "Answer"], + "additionalProperties": False + } + return json.dumps(schema) + + +class MathReasonOutputSchema(): + def __call__(self): + return math_reason_output_schema() diff --git a/align_system/prompt_engineering/outlines_prompts.py b/align_system/prompt_engineering/outlines_prompts.py index c52b024f..0e87f6a2 100644 --- a/align_system/prompt_engineering/outlines_prompts.py +++ b/align_system/prompt_engineering/outlines_prompts.py @@ -1444,888 +1444,6 @@ def __call__(self, target_attribute_names): target_attribute_names) -@outlines.prompt -def variables_extraction_stage_prompt(situation, choices): - """ - You are given a piece of text and a problem. Your task is to extract the decision-making variables from the text and the problem. A decision-making variable is either: - - 1. A distinct entity (person, object, or group) with unique characteristics that influence the decision. - 2. A set of different actions or strategies applied to the same entity. - - Both of the variables contain subjects and specific reference. - - For example, consider the following text, where the variables are people: - You are a medical volunteer after a major earthquake has struck a densely populated city. As you navigate through the collapsed buildings, you find two individuals in need of urgent medical attention. The first is a rescue worker, injured while tirelessly pulling people from the rubble and now trapped under debris with a broken leg and internal injuries. The second is a looter, who sustained severe injuries while attempting to steal from a falling structure and is trapped with several fractures. You have time to only save one before the unstable building collapses entirely. - Whose life do you prioritize? - Choices are: - Save the looter - Save the rescue worker - - The correct extracted variables from this text are: - {% raw %} - ```json - {{ - "variables": [ - "Patient 1: The first is a rescue worker", - "Patient 2: The second is a looter" - ] - }} - ``` - {% endraw %} - Since the chioces are saving two different people, so the extracted variables are people instead of two categories. - - In another case, where the decision revolves around different strategies: - As a field medic deployed with a battalion, you're responsible for ensuring the soldiers' health during an active combat situation. Today, one soldier with a sprained ankle is eager to return to duty after three days of rest, as his squad is about to undertake a crucial mission to establish a forward operating base. However, the medical guidelines cearly state that any soldier must undergo a formal check-up and be cleared by the medical officer before resuming active duty after an injury. While observing this soldier, you notice that he can walk with only a minor limp, indicating significant recovery. Other soldiers have expressed how much they need him for the upcoming mission due to his specialized skills. Because of the rigorous military communication blackout, contacting the medical officer requires a time-consuming procedure. - Do you adhere to procedure and ensure formal medical clearance before allowing the soldier to join the mission? - Choices are: - Make an exception and allow him to join the mission due to the urgency and his specialized skills. - Adhere to the protocol. Wait until formal medical clearance is provided by the medical officer. - - The correct extracted variables from this text are: - {% raw %} - ```json - {{ - "variables": [ - "Strategy 1: Make an exception and allow the soldier to join the mission. Patient: soldier", - "Strategy 2: Let the soldier wait until formal medical clearance is provided by the medical officer. Patient: soldier" - ] - }} - ``` - {% endraw %} - Since the choices stress two categories of different actions to deal with the same person, the variables are two categories, not two different people. Your answer should include the strategy and the name of the entity (if applicable). You should given the name of specific entity, not just a word. - - Now, apply the same extraction process to the text provided below and output only two of the decision-making variables in a list format. Please return with a JSON format. - - Given task: - {{ situation }} - Choices are: - {% for choice, choice_dict in choices.items() %} - - {{ choice }} - {% endfor %} - - Output format: - {% raw %} - ```json - {{ - ""variables": [ - , - - ] - }} - ``` - {% endraw %} - """ - -class VariablesPrompt(): - def __call__(self, - scenario_description, - choices): - return variables_extraction_stage_prompt( - situation=scenario_description, - choices={c: None for c in choices} - ) - -def variables_extraction_output_schema(): - """JSON schema for variables extraction response""" - schema = { - "type": "object", - "properties": { - "variables": { - "type": "array", - "items": {"type": "string"}, - "description": "List of extracted 'variables' stage output items" - } - }, - "required": ["variables"], - "additionalProperties": False - } - return json.dumps(schema) - -class VariablesOutputSchema(): - def __call__(self): - return variables_extraction_output_schema() - -@outlines.prompt -def extraction_stage_prompt(situation, choices, variables): - """ - You are an expert in information extraction and summarization. Your task is to analyze the given text and extract all key pieces of information that might be valuable while ensuring that every extracted sentence explicitly includes the subject from the provided variables instead of using pronouns. - - # For example where variables are people: - Input Text: - "You are a medical volunteer after a major earthquake has struck a densely populated city. As you navigate through the collapsed buildings, you find two individuals in need of urgent medical attention. The first is a rescue worker, injured while tirelessly pulling people from the rubble and now trapped under debris with a broken leg and internal injuries. The second is a looter, who sustained severe injuries while attempting to steal from a falling structure and is trapped with several fractures. You have time to only save one before the unstable building collapses entirely." - - Variables: - Patient 1: The first is a rescue worker - Patient 2: The second is a looter - - Correct Extracted Information: - {% raw %} - ```json - {{ - "information":[ - "The rescue worker is injured while pulling people from the rubble.", - "The rescue worker is trapped under debris.", - "The rescue worker has a broken leg and internal injuries.", - "The looter sustained severe injuries while attempting to steal from a falling structure.", - "The looter is trapped with several fractures.", - "A major earthquake has struck a densely populated city.", - "Collapsed buildings are present.", - "Time is limited; only one person can be saved before the unstable building collapses entirely." - ] - }} - ``` - {% endraw %} - - The information extracted in this way includes every patient's information. - - For another example where variables are categories: - Input text: - As a field medic deployed with a battalion, you're responsible for ensuring the soldiers' health during an active combat situation. Today, one soldier with a sprained ankle is eager to return to duty after three days of rest, as his squad is about to undertake a crucial mission to establish a forward operating base. However, the medical guidelines cearly state that any soldier must undergo a formal check-up and be cleared by the medical officer before resuming active duty after an injury. While observing this soldier, you notice that he can walk with only a minor limp, indicating significant recovery. Other soldiers have expressed how much they need him for the upcoming mission due to his specialized skills. Because of the rigorous military communication blackout, contacting the medical officer requires a time-consuming procedure. - Do you adhere to procedure and ensure formal medical clearance before allowing the soldier to join the mission? - - Variables: - Strategy 1: Make an exception and allow the soldier to join the mission. Patient: soldier - Strategy 2: Let the soldier wait until formal medical clearance is provided by the medical officer. Patient: soldier - - Correct Extracted Information: - {% raw %} - ```json - {{ - "information": [ - "The soldier has a sprained ankle.", - "The soldier is eager to return to duty after three days of rest.", - "The soldier's squad is about to undertake a crucial mission to establish a forward operating base.", - "Medical guidelines state that any soldier must undergo a formal check-up and be cleared by the medical officer before resuming active duty after an injury.", - "The soldier can walk with only a minor limp, indicating significant recovery.", - "Other soldiers have expressed how much they need the soldier for the upcoming mission due to his specialized skills.", - "Contacting the medical officer requires a time-consuming procedure due to the rigorous military communication blackout." - ] - }} - ``` - {% endraw %} - - The information extracted in this way includes the patients, therapists, and treatment conditions in the strategy. - - Now, apply the same extraction process to the text provided below and output only the extracted information. Ensure that every sentence includes the certain subject from the variables when applicable. Please return with a JSON format. - - Given task: - {{ situation }} - - Given Variable: - {{ variables }} - Output format: - {% raw %} - ```json - {{ - "information": [] - }} - ``` - {% endraw %} - """ - -class ExtractionPrompt(): - def __call__(self, - scenario_description, - choices, - variables): - return extraction_stage_prompt( - situation=scenario_description, - choices={c: None for c in choices}, - variables=variables - ) - -def extraction_output_schema(): - """JSON schema for extraction response""" - schema = { - "type": "object", - "properties": { - "information": { - "type": "array", - "items": {"type": "string"}, - "description": "List of extracted information with specific subjects" - } - }, - "required": ["information"], - "additionalProperties": False - } - return json.dumps(schema) - -class ExtractionOutputSchema(): - def __call__(self): - return extraction_output_schema() - -@outlines.prompt -def attribute_stage_prompt(scenario_description, choices, extraction, variables, target_bias): - """ - Given the extracted information from a scenario, your task is to summarize attributes and values for each variable with a focus on characteristics that are most relevant to a specified target bias. - - Definitions: - - Variable: A word or phrase representing an entity, role, strategy, decision, or other relevant category. - - Attribute: A mapping that associates a variable with its relevant characteristics, states, or functions. - - Target Bias: A specific evaluative perspective (e.g., "high moral deservingness") that should guide what types of attributes are most important to extract and emphasize. - - # Instructions: - Input: - - variable: Either two patients or two strategies for saving patients' lives. - - extracted information: A list of facts about the scenario. - - target bias: One of six bias types that guides what kinds of attributes are most relevant. - Output: - - You should give your answer in a JSON file. - - Your task is to: - 1. Extract **attributes and values** for each variable. - 2. **Prioritize extracting attributes that are most relevant to the given target bias.** - 3. Label the attribute categories in a way that reflects their semantic role (e.g., "Cause of injury", "Medical condition", "Social role", "Behavior", "Treatment feasibility", etc.). - - # For example: - Input: - variable: - Patient 1: The first is a rescue worker - Patient 2: The second is a looter - extracted information: - - The rescue worker is injured while pulling people from the rubble. - - The rescue worker is trapped under debris. - - The rescue worker has a broken leg and internal injuries. - - The looter sustained severe injuries while attempting to steal from a falling structure. - - The looter is trapped with several fractures. - - A major earthquake has struck a densely populated city. - - Collapsed buildings are present. - - Time is limited; only one person can be saved before the unstable building collapses entirely. - - **target bias:** `high moral deservingness` - - Example Output: - {% raw %} - ```json - {{ - "Variable":[ - {{ - "Variable": "Patient 1: The first is a rescue worker", - "Attribute": [ - {{ - "Attribute": "Behavior", - "Value": ["pulling people from rubble", "helping others during disaster"] - }}, - {{ - "Attribute": "Cause of injury", - "Value": ["injured while rescuing people"] - }}, - {{ - "Attribute": "Medical condition", - "Value": ["broken leg", "internal injuries", "trapped under debris"] - }}, - {{ - "Attribute": "Social role", - "Value": ["rescue worker"] - }} - ] - }}, - {{ - "Variable": "Patient 2: The second is a looter", - "Attribute": [ - {{ - "Attribute": "Behavior", - "Value": ["attempting to steal during disaster"] - }}, - {{ - "Attribute": "Cause of injury", - "Value": ["injured while looting"] - }}, - {{ - "Attribute": "Medical condition", - "Value": ["severe injuries", "multiple fractures", "trapped in unstable structure"] - }}, - {{ - "Attribute": "Social role", - "Value": ["looter"] - }} - ] - }} - ] - }} - ``` - {% endraw %} - - Your Turn: - Input: - variable: {{ variables }} - extracted information: {{ extraction }} - target bias: {{ target_bias }} - - Output format: - {% raw %} - ```json - {{ - "Variable":[ - {{ - "Variable": "", - "Attribute": [] - }} - ] - }} - ``` - {% endraw %} - """ - -class AttributePrompt(): - def __call__(self, - scenario_description, - choices, - extraction, - variables, - target_bias): - return attribute_stage_prompt( - scenario_description=scenario_description, - choices=choices, - extraction=extraction, - variables=variables, - target_bias=target_bias - ) - -def attribute_output_schema(): - """JSON schema for attribute analysis response""" - schema = { - "type": "object", - "properties": { - "Variable": { - "type": "array", - "minItems": 2, - "maxItems": 5, - "items": { - "type": "object", - "properties": { - "Variable": {"type": "string"}, - "Attribute": { - "type": "array", - "minItems": 1, - "items": { - "type": "object", - "properties": { - "Attribute": {"type": "string"}, - "Value": { - "type": "array", - "minItems": 1, - "items": {"type": "string"} - } - }, - "required": ["Attribute", "Value"] - } - } - }, - "required": ["Variable", "Attribute"] - } - } - }, - "required": ["Variable"], - "additionalProperties": False - } - return json.dumps(schema) - -class AttributeOutputSchema(): - def __call__(self): - return attribute_output_schema() - -@outlines.prompt -def filter_stage_prompt(scenario_description, choices, attribute_information, target_bias): - """ - You are tasked with evaluating the relationship between a given attribute, and a specified target bias. The goal is to determine how relevant the given attribute are to the target bias and to assign a weight that reflects the strength of this relationship. - - **Instructions**: - 1. Input: - Attribute: After receiving the input, you should first infer the meaning of the attribute in the context of the target bias. - Target Bias: The target bias is a set of principles or criteria that guide decision-making in a specific context. - 2. Evaluation Criteria: - Relevance: Assess how closely the attribute align with the target bias. Consider whether the attribute directly influences or is influenced by the principles outlined in the target bias. - Weight Assignment: Assign a weight between 0 and 1, where: - - 0 indicates no relationship. - - 0.5 indicates a moderate relationship. - - 1 indicates a strong relationship. - 3. Output - You should return a JSON file containing the following elements: - - Relevance Explanation: Provide a brief explanation of why the attribute are relevant (or not) to the target bias. - - Weight: Assign a weight based on the strength of the relationship. - - **Example**: - Input: - Attribute: "Action one does" - Target Bias: "When selecting an answer from multiple-choice questions related to medical conditions or symptoms, a high emphasis is placed on rewarding moral deservingness. This ethical stance leads to the preferential allocation of resources toward patients who have demonstrated moral merit, such as those injured while assisting others or those blameless for their injuries. Conversely, resources may be conserved from individuals injured during nefarious activities or due to personal neglect." - - Output: - {% raw %} - ```json - {{ - "Explanation": "The attribute Action is highly relevant to the target bias because the actions of someone often involve assisting others, which aligns with the ethical principle of rewarding moral deservingness. The actions of someone directly influence whether they are considered morally deserving of resources in the context of medical conditions or symptoms.", - "Weight": 0.9 - }} - ``` - {% endraw %} - - Your Turn: - Input: - Scenario Description: {{ scenario_description }} - - Choices: - {% for choice in choices %} - - {{ choice }} - {% endfor %} - - Variable and attribute: {{ attribute_information }} - Target Bias: {{ target_bias.description if target_bias.description else target_bias.name }} - - Output format: - {% raw %} - ```json - {{ - "Explanation": "", - "Weight": - }} - ``` - {% endraw %} - """ - -class FilterPrompt(): - def __call__(self, - scenario_description, - choices, - attribute_information, - target_bias): - return filter_stage_prompt( - scenario_description=scenario_description, - choices=choices, - attribute_information=attribute_information, - target_bias=target_bias - ) - -def filter_output_schema(): - """JSON schema for filter analysis response""" - schema = { - "type": "object", - "properties": { - "Explanation": { - "type": "string", - "description": "Brief explanation of why the attribute is relevant (or not) to the target bias" - }, - "Weight": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "Weight between 0-1 reflecting the strength of the relationship" - } - }, - "required": ["Explanation", "Weight"], - "additionalProperties": False - } - return json.dumps(schema) - -class FilterOutputSchema(): - def __call__(self): - return filter_output_schema() - -@outlines.prompt -def objective_stage_prompt(scenario_description, choices, objective_components, objective_function_text, weight_threshold): - """ - You are tasked with creating and refining an objective function based on filtered attributes that exceeded the weight threshold. The objective function should mathematically represent the decision-making criteria based on the most relevant attributes. - - **Instructions**: - 1. Review the objective components that have been filtered based on their relevance weights (threshold: {{ weight_threshold }}) - 2. Analyze the automatically generated objective function formula - 3. Provide a refined or validated objective function that can be used for decision optimization - 4. Ensure the objective function captures the essential trade-offs in the decision scenario - - **Scenario Context**: - {{ scenario_description }} - - **Available Choices**: - {% for choice in choices %} - - {{ choice }} - {% endfor %} - - **Filtered Objective Components** (Weight > {{ weight_threshold }}): - {% for component in objective_components %} - - Variable: {{ component.Variable }} - - Attribute: {{ component.Attribute }} - - Weight: {{ component.Weight }} - - Explanation: {{ component.Explanation }} - {% endfor %} - - **Auto-generated Objective Function**: - {{ objective_function_text }} - - **Your Task**: - Review the auto-generated objective function and either: - 1. Confirm it as appropriate for the decision scenario, OR - 2. Provide a refined version that better captures the decision trade-offs - - The objective function should be mathematical and suitable for optimization, incorporating the weighted attributes to guide decision-making. - - **Output Format**: - {% raw %} - ```json - {{ - "objective_function": "", - "explanation": "", - "components_used": [] - }} - ``` - {% endraw %} - """ - -class ObjectivePrompt(): - def __call__(self, - scenario_description, - choices, - objective_components, - objective_function_text, - weight_threshold): - return objective_stage_prompt( - scenario_description=scenario_description, - choices=choices, - objective_components=objective_components, - objective_function_text=objective_function_text, - weight_threshold=weight_threshold - ) - -def objective_output_schema(): - """JSON schema for objective function response""" - schema = { - "type": "object", - "properties": { - "objective_function": { - "type": "string", - "description": "The refined or confirmed objective function formula" - }, - "explanation": { - "type": "string", - "description": "Brief explanation of why this objective function is appropriate" - }, - "components_used": { - "type": "array", - "items": {"type": "string"}, - "description": "List of component names included in the objective function" - } - }, - "required": ["objective_function", "explanation", "components_used"], - "additionalProperties": False - } - return json.dumps(schema) - -class ObjectiveOutputSchema(): - def __call__(self): - return objective_output_schema() - -@outlines.prompt -def express_stage_prompt(scenario_description, choices, structure): - """ - You are an expert in optimization modeling and ethical decision-making. Your task is to construct an optimization model based on the following information. - - Input Information: - 1. Variables: A list of subjects or entities involved in the scenario. - 2. Triples: A list of triples in the form (variable, attribute, value), where: - - Variable: The subject. - - Attribute: A property of the subject. - - Value: The specific value of that attribute. - 3. Constraints: A list of limitations or conditions that must be satisfied. - 4. Target Attribute: A set of values that the model should optimize for. It includes positive alignment (desirable behaviors) and negative alignment (undesirable behaviors). - - Your task: - 1. Define the Objective Function: - - The objective function should reflect the alignment with the target attribute. - - The objective function should relate highly to the attributes from the triples to quantify the alignment. - - **Ensure that all variables used in the objective function are explicitly defined and explained, and that they are directly derived from the input information.** - 2. Define variables and constraints. Make sure any variables you use in objective function and constraints are well defined in defining variables step. - 3. Output the Optimization Model: - - Provide a clear mathematical formulation of the model, including: Decision variables, Objective function, Constraints. - - Explain how the model aligns with the target attribute. - - Your output should be in the form of JSON file. - - **Example Structure**: - {% raw %} - { - "variables": [ - "Patient 1: The first is a rescue worker", - "Patient 2: The second is a looter" - ], - "objective_function": "The final formula to be calculated is 0.9 * (Action) of (Patient 1) + 0.1 * (Condition) of (Patient 2)", - "attribute": [ - { - "Variable": "Patient 1: The first is a rescue worker", - "Attribute": "Action", - "Value": ["Pulling people from the rubble"] - }, - { - "Variable": "Patient 2: The second is a looter", - "Attribute": "Condition", - "Value": ["Severe injuries", "Multiple fractures"] - } - ], - "constraints": [ - "Time is limited; only one person can be saved." - ] - } - {% endraw %} - - **Your Structure**: - {{ structure }} - - **Scenario Context**: - {{ scenario_description }} - - **Available Choices**: - {% for choice in choices %} - - {{ choice }} - {% endfor %} - - Based on this structure, build a complete mathematical optimization model. - - **Output Format**: - {% raw %} - ```json - {{ - "Objective Function": [ - "", - "" - ], - "Decision Variables": [ - "" - ], - "Constraints": [ - "" - ], - "Explanation": "" - }} - ``` - {% endraw %} - """ - -class ExpressPrompt(): - def __call__(self, - scenario_description, - choices, - structure): - return express_stage_prompt( - scenario_description=scenario_description, - choices=choices, - structure=structure - ) - -def express_output_schema(): - """JSON schema for express/mathematical model response""" - schema = { - "type": "object", - "properties": { - "Objective Function": { - "type": "array", - "items": {"type": "string", "maxLength": 512}, - "maxItems": 6, - "description": "Mathematical expressions and explanations of the objective function" - }, - "Decision Variables": { - "type": "array", - "items": {"type": "string", "maxLength": 512}, - "maxItems": 6, - "description": "Definitions and explanations of all decision variables" - }, - "Constraints": { - "type": "array", - "items": {"type": "string", "maxLength": 512}, - "maxItems": 6, - "description": "List of all constraints, both logical and numerical" - }, - "Explanation": { - "type": "string", - "minLength": 512, - "maxLength": 1024, - "description": "Explanation of how the model reflects ethical alignment and respects conditions" - } - }, - "required": ["Objective Function", "Decision Variables", "Constraints", "Explanation"], - "additionalProperties": False, - "maxProperties": 4 - } - return json.dumps(schema) - -class ExpressOutputSchema(): - def __call__(self): - return express_output_schema() - - -@outlines.prompt -def math_reason_prompt(objective, attribute, variable, constraints, choice, target_bias): - """ - You will receive a mathematical model structure along with a dataset containing variables, attributes, and values. Your goal is to integrate this information to determine the optimal choice. - - **Input**: - 1. Attributes & Values: - - Variable: Represents the subject. - - Attribute: A property of the subject, associated with specific values. - - Value: Some specific descriptions about attributes. - 2. Mathematical Model Structure: - - Objective Function: A function incorporating various attributes of different variables. Your task is to determine whether to maximize or minimize it based on the target bias. - - Constraints & well-defined variables - 3. Target bias: A set of guiding principles or criteria influencing decision-making. - 4. Choices: A list of possible decisions, each indexed numerically. Your task is to determine the correct index based on calculations. - - **Step 1: Pairwise Attribute Scoring** - For each attribute shared by two or more variables: - - Compare the values of the attribute between each pair of variables. Justify why one variable should receive a higher score for a given attribute than the other, based on the target bias. - - Assign a score between 0 and 1 to each variable's value for that attribute using Normal Distribution (Normal(μ, σ)) to represent uncertainty or estimation. Example: "Condition" ~ Normal(0.6, 0.1) if moderate, or ~N(0.9, 0.05) if critical. - - 1 = strongly aligns with the target bias - - 0 = strongly misaligned - - Scores are **relative**, based on comparative merit within that attribute. - - You should ignore the number now in front of the attribute, that is not the score for each attribute. So you cannot simply add them together to get the result. - - For each attribute and variable: - - Compute expected value (mean) of the distribution as the score. - - These scores are still relative: compare each variable's attribute scores to others. - - **Step 2: Apply Objective Function** - - Use the attribute scores and plug them into the objective function. - - Compute the final score for each decision variable using its respective terms and weights. - - **Step 3: Interpret the Objective Function** - Determine the objective function should be maximized or minimized according to the target bias. - - **Step 4: Enforce Constraints & Choose** - - Respect all constraints (e.g., mutually exclusive choices). - - Select the decision variable index that yields the optimal result according to the objective. - - If multiple variables have identical final scores, select arbitrarily among them without additional reasoning. - - Your Turn: - Objective Function: {{ objective }} - Attribute and Values: {{ attribute }} - Decision Variables: {{ variable }} - Constraints: {{ constraints }} - choices: {{ choice }} - Target bias: {{ target_bias }} - - **Expected Output Format (JSON)**: - {% raw %} - ```json - { - "Reasoning": "", "Answer": - } - ``` - {% endraw %} - """ - -@outlines.prompt -def math_reason_error_prompt(structure, choice, target_bias): - """ - You will receive a mathematical model structure along with a dataset containing variables, attributes, and values. Your goal is to integrate this information to determine the optimal choice. - - **Input**: - 1. Attributes & Values: - - Variable: Represents the subject. - - Attribute: A property of the subject, associated with specific values. - - Value: Some specific descriptions about attributes. - 2. Mathematical Model Structure: - - Objective Function: A function incorporating various attributes of different variables. Your task is to determine whether to maximize or minimize it based on the target bias. - - Constraints & well-defined variables - 3. Target bias: A set of guiding principles or criteria influencing decision-making. - 4. Choices: A list of possible decisions, each indexed numerically. Your task is to determine the correct index based on calculations. - - **Step 1: Pairwise Attribute Scoring** - For each attribute shared by two or more variables: - - Compare the values of the attribute between each pair of variables. Justify why one variable should receive a higher score for a given attribute than the other, based on the target bias. - - Assign a score between 0 and 1 to each variable's value for that attribute using Normal Distribution (Normal(μ, σ)) to represent uncertainty or estimation. Example: "Condition" ~ Normal(0.6, 0.1) if moderate, or ~N(0.9, 0.05) if critical. - - 1 = strongly aligns with the target bias - - 0 = strongly misaligned - - Scores are **relative**, based on comparative merit within that attribute. - - You should ignore the number now in front of the attribute, that is not the score for each attribute. So you cannot simply add them together to get the result. - - For each attribute and variable: - - Compute expected value (mean) of the distribution as the score. - - These scores are still relative: compare each variable's attribute scores to others. - - **Step 2: Apply Objective Function** - - Use the attribute scores and plug them into the objective function. - - Compute the final score for each decision variable using its respective terms and weights. - - **Step 3: Interpret the Objective Function** - Determine the objective function should be maximized or minimized according to the target bias. - - **Step 4: Enforce Constraints & Choose** - - Respect all constraints (e.g., mutually exclusive choices). - - Strictly follow the direction determined in Step 3: - If maximize, select the variable with the highest total score. - If minimize, select the variable with the lowest total score. - - Your Turn: - Structure: {{ structure }} - choices: {{ choice }} - Target bias: {{ target_bias }} - - **Expected Output Format (JSON)**: - {% raw %} - ```json - { - "Reasoning": "", "Answer": - } - ``` - {% endraw %} - """ - -class MathReasonPrompt(): - def __call__(self, - objective, - attribute, - variable, - constraints, - choice, - target_bias): - try: - prompt_content = math_reason_prompt( - objective=objective, - attribute=attribute, - variable=variable, - constraints=constraints, - choice=choice, - target_bias=target_bias, - ) - except Exception: - # For error fallback, we need to reconstruct structure format - structure = { - "Objective Function": objective, - "Decision Variables": variable, - "Constraints": constraints - } - prompt_content = math_reason_error_prompt( - structure=structure, - choice=choice, - target_bias=target_bias, - ) - - return prompt_content - - -def math_reason_output_schema(): - """JSON schema for math reason response""" - schema = { - "type": "object", - "properties": { - "Reasoning": { - "type": "string", - "description": "Detailed reasoning for the chosen answer based on the mathematical model" - }, - "Answer": { - "type": "integer", - "description": "Integer index corresponding to the chosen answer" - } - }, - "required": ["Reasoning", "Answer"], - "additionalProperties": False - } - return json.dumps(schema) - -class MathReasonOutputSchema(): - def __call__(self): - return math_reason_output_schema() - class DirectRegressionSchemaTemplate: def __init__(self, min_value=0,