""" Output formatter component for causal inference results. This module formats the results of causal analysis into a clear, structured output for presentation to the user. """ from typing import Dict, List, Any, Optional import json # Add this import at the top of the file # Import the new model from auto_causal.models import FormattedOutput # Add this module-level variable, typically near imports or at the top CURRENT_OUTPUT_LOG_FILE = None # Revert signature and logic to handle results and structured explanation def format_output( query: str, method: str, results: Dict[str, Any], explanation: Dict[str, Any], dataset_analysis: Optional[Dict[str, Any]] = None, dataset_description: Optional[str] = None ) -> FormattedOutput: """ Format final results including numerical estimates and explanations. Args: query: Original user query method: Causal inference method used (string name) results: Numerical results from method_executor_tool explanation: Structured explanation object from explainer_tool dataset_analysis: Optional dictionary of dataset analysis results dataset_description: Optional string description of the dataset Returns: Dict with formatted output fields ready for presentation. """ # Extract numerical results effect_estimate = results.get("effect_estimate") confidence_interval = results.get("confidence_interval") p_value = results.get("p_value") effect_se = results.get("standard_error") # Get SE if available # Format method name for readability method_name_formatted = _format_method_name(method) # Extract explanation components (assuming explainer returns structured dict again) # If explainer returns single string, adjust this method_explanation_text = explanation.get("method_explanation", "") interpretation_guide = explanation.get("interpretation_guide", "") limitations = explanation.get("limitations", []) assumptions_discussion = explanation.get("assumptions", "") # Assuming key is 'assumptions' practical_implications = explanation.get("practical_implications", "") # Add back final_explanation_text if explainer provides it # final_explanation_text = explanation.get("final_explanation_text") # Create summary using numerical results ci_text = "" if confidence_interval and confidence_interval[0] is not None and confidence_interval[1] is not None: ci_text = f" (95% CI: [{confidence_interval[0]:.4f}, {confidence_interval[1]:.4f}])" p_value_text = f", p={p_value:.4f}" if p_value is not None else "" effect_text = f"{effect_estimate:.4f}" if effect_estimate is not None else "N/A" summary = ( f"Based on {method_name_formatted}, the estimated causal effect is {effect_text}" f"{ci_text}{p_value_text}. {_create_effect_interpretation(effect_estimate, p_value)}" f" See details below regarding assumptions and limitations." ) # Assemble formatted output dictionary results_dict = { "query": query, "method_used": method_name_formatted, "causal_effect": effect_estimate, "standard_error": effect_se, "confidence_interval": confidence_interval, "p_value": p_value, "summary": summary, "method_explanation": method_explanation_text, "interpretation_guide": interpretation_guide, "limitations": limitations, "assumptions": assumptions_discussion, "practical_implications": practical_implications, # "full_explanation_text": final_explanation_text # Optionally include combined text } final_results_dict = {key : results_dict[key] for key in {"query", "method_used", "causal_effect", "standard_error", "confidence_interval"}} # print(final_results_dict) # Validate and instantiate the Pydantic model try: formatted_output_model = FormattedOutput(**results_dict) except Exception as e: # Catch validation errors specifically if needed # Handle validation error - perhaps log and return a default or raise print(f"Error creating FormattedOutput model: {e}") # Or use logger # Decide on error handling: raise, return None, return default? # For now, re-raising might be simplest if the structure is expected raise ValueError(f"Failed to create FormattedOutput from results: {e}") return formatted_output_model # Return the Pydantic model instance def _format_method_name(method: str) -> str: """Format method name for readability.""" method_names = { "propensity_score_matching": "Propensity Score Matching", "regression_adjustment": "Regression Adjustment", "instrumental_variable": "Instrumental Variable Analysis", "difference_in_differences": "Difference-in-Differences", "regression_discontinuity": "Regression Discontinuity Design", "backdoor_adjustment": "Backdoor Adjustment", "propensity_score_weighting": "Propensity Score Weighting" } return method_names.get(method, method.replace("_", " ").title()) # Reinstate helper function for interpretation def _create_effect_interpretation(effect: Optional[float], p_value: Optional[float] = None) -> str: """Create a basic interpretation of the effect.""" if effect is None: return "Effect estimate not available." significance = "" if p_value is not None: significance = "statistically significant" if p_value < 0.05 else "not statistically significant" magnitude = "" if abs(effect) < 0.01: magnitude = "no practical effect" elif abs(effect) < 0.1: magnitude = "a small effect" elif abs(effect) < 0.5: magnitude = "a moderate effect" else: magnitude = "a substantial effect" return f"This suggests {magnitude}{f' and is {significance}' if significance else ''}."