In /root/projects/butler, do a thorough exploration of the existing analytics and data analysis capabilities. Look at:

1. health/analytics/ - all files, what analysis engines exist
2. health/services/query.py - what query methods exist
3. slack_bot/tools/health_read.py - what analysis tools are exposed to the LLM
4. slack_bot/tools/registry.py - full list of tools with descriptions
5. Any correlation, trend, or insight generation logic

For each analysis capability found, describe:
- What it analyzes
- What data sources it uses
- What it outputs
- Any limitations

Also note what data types are available: steps, heart_rate, sleep, stress, body_battery, spo2, respiration, hydration, floors, intensity_minutes, hrv, rhr, weight, activities, AND the newly added lifestyle_logging (alcohol, caffeine, meals, fasting, light exercise).

Return a comprehensive list of existing analysis capabilities and clear gaps where analysis could be added.


I'll conduct a thorough exploration of the analytics and data analysis capabilities in the butler project. Let me start by examining the key files.


     1→"""
     2→Query service for health data analysis.
     3→
     4→Provides methods to query and analyze health data from storage.
     5→"""
     6→
     7→import json
     8→from datetime import date, timedelta
     9→from pathlib import Path
    10→from typing import Dict, List, Optional, Any, Union
    11→
    12→from health import config
    13→from health.db.repository import HealthRepository
    14→from health.services.storage import HealthStorage
    15→from health.utils.logging_config import setup_logger
    16→
    17→logger = setup_logger(__name__)
    18→
    19→
    20→class HealthDataQuery:
    21→    """Service for querying and analyzing health data."""
    22→
    23→    def __init__(
    24→        self, storage: Optional[HealthStorage] = None, repo: Optional[HealthRepository] = None
    25→    ) -> None:
    26→        """Initialize query service.
    27→
    28→        Args:
    29→            storage: Optional storage service instance
    30→            repo: Optional repository instance
    31→        """
    32→        self.storage = storage or HealthStorage()
    33→        self.repo = repo or HealthRepository()
    34→
    35→    def get_daily_summary(self, target_date: date) -> Dict[str, Any]:
    36→        """Get a summary of all health metrics for a specific day.
    37→
    38→        Args:
    39→            target_date: Date to query
    40→
    41→        Returns:
    42→            Dictionary containing all available metrics for the day
    43→        """
    44→        logger.info(f"Querying health data summary for {target_date}")
    45→
    46→        summary = {
    47→            "date": target_date.isoformat(),
    48→            "metrics": {},
    49→            "activities": [],
    50→        }
    51→
    52→        # Query all daily metrics
    53→        for metric_type in config.DATA_TYPE_CONFIG.keys():
    54→            # Skip activities (handled separately) and weight (body metric)
    55→            if metric_type in ["activities", "weight"]:
    56→                continue
    57→
    58→            data = self.storage.load_daily_metric(metric_type, target_date)
    59→            if data:
    60→                summary["metrics"][metric_type] = data
    61→
    62→        # Query activities for the day
    63→        activities = self.repo.get_activities_by_date_range(target_date, target_date)
    64→        for activity in activities:
    65→            activity_data = self.storage.load_activity(activity["activity_id"])
    66→            if activity_data:
    67→                summary["activities"].append(activity_data)
    68→
    69→        # Query weight (if available)
    70→        weight_data = self.storage.load_daily_metric("weight", target_date)
    71→        if weight_data:
    72→            summary["metrics"]["weight"] = weight_data
    73→
    74→        logger.info(
    75→            f"Found {len(summary['metrics'])} metrics and {len(summary['activities'])} activities"
    76→        )
    77→
    78→        return summary
    79→
    80→    def get_metric_range(
    81→        self, metric_type: str, start_date: date, end_date: date
    82→    ) -> List[Dict[str, Any]]:
    83→        """Get a specific metric's data over a date range.
    84→
    85→        Args:
    86→            metric_type: Type of metric (e.g., "steps", "sleep", "heart_rate")
    87→            start_date: Start date of range
    88→            end_date: End date of range
    89→
    90→        Returns:
    91→            List of metric data dictionaries, ordered by date
    92→
    93→        Raises:
    94→            ValueError: If metric_type is invalid
    95→        """
    96→        if metric_type not in config.DATA_TYPE_CONFIG:
    97→            valid_types = ", ".join(config.DATA_TYPE_CONFIG.keys())
    98→            raise ValueError(
    99→                f"Invalid metric type: {metric_type}. Valid types: {valid_types}"
   100→            )
   101→
   102→        logger.info(f"Querying {metric_type} from {start_date} to {end_date}")
   103→
   104→        results = []
   105→        current_date = start_date
   106→
   107→        while current_date <= end_date:
   108→            data = self.storage.load_daily_metric(metric_type, current_date)
   109→            if data:
   110→                results.append(data)
   111→            current_date += timedelta(days=1)
   112→
   113→        logger.info(f"Found {len(results)} records for {metric_type}")
   114→
   115→        return results
   116→
   117→    def get_activities_range(
   118→        self, start_date: date, end_date: date, activity_type: Optional[str] = None
   119→    ) -> List[Dict[str, Any]]:
   120→        """Get all activities within a date range.
   121→
   122→        Args:
   123→            start_date: Start date of range
   124→            end_date: End date of range
   125→            activity_type: Optional filter by activity type
   126→
   127→        Returns:
   128→            List of activity data dictionaries
   129→        """
   130→        logger.info(f"Querying activities from {start_date} to {end_date}")
   131→
   132→        # Get activity index entries
   133→        activity_indices = self.repo.get_activities_by_date_range(start_date, end_date)
   134→
   135→        # Load full activity data
   136→        activities = []
   137→        for idx in activity_indices:
   138→            # Filter by activity type if specified
   139→            if activity_type and idx.get("activity_type") != activity_type:
   140→                continue
   141→
   142→            activity_data = self.storage.load_activity(idx["activity_id"])
   143→            if activity_data:
   144→                activities.append(activity_data)
   145→
   146→        logger.info(f"Found {len(activities)} activities")
   147→
   148→        return activities
   149→
   150→    def get_metric_statistics(
   151→        self, metric_type: str, start_date: date, end_date: date, fields: List[str]
   152→    ) -> Dict[str, Dict[str, Any]]:
   153→        """Calculate statistics for specific metric fields over a date range.
   154→
   155→        Args:
   156→            metric_type: Type of metric
   157→            start_date: Start date of range
   158→            end_date: End date of range
   159→            fields: List of field names to calculate statistics for
   160→
   161→        Returns:
   162→            Dictionary mapping field names to their statistics (min, max, avg, count)
   163→        """
   164→        logger.info(
   165→            f"Calculating statistics for {metric_type} fields: {fields} from {start_date} to {end_date}"
   166→        )
   167→
   168→        # Get all data for the range
   169→        data_list = self.get_metric_range(metric_type, start_date, end_date)
   170→
   171→        # Calculate statistics for each field
   172→        stats = {}
   173→        for field in fields:
   174→            values = []
   175→            for data in data_list:
   176→                if field in data and data[field] is not None:
   177→                    values.append(data[field])
   178→
   179→            if values:
   180→                stats[field] = {
   181→                    "min": min(values),
   182→                    "max": max(values),
   183→                    "avg": sum(values) / len(values),
   184→                    "count": len(values),
   185→                    "sum": sum(values),
   186→                }
   187→            else:
   188→                stats[field] = {
   189→                    "min": None,
   190→                    "max": None,
   191→                    "avg": None,
   192→                    "count": 0,
   193→                    "sum": None,
   194→                }
   195→
   196→        return stats
   197→
   198→    def get_available_dates(self, metric_type: str) -> List[str]:
   199→        """Get all available dates for a metric type.
   200→
   201→        Args:
   202→            metric_type: Type of metric
   203→
   204→        Returns:
   205→            List of date strings in ISO format
   206→        """
   207→        # This is a simplified implementation - in a real scenario,
   208→        # you might want to query the database index for better performance
   209→        storage_path = config.DATA_TYPE_CONFIG[metric_type]["storage_path"]
   210→        base_path = config.DATA_DIR / storage_path
   211→
   212→        dates = []
   213→        if base_path.exists():
   214→            # Walk through year/month directories
   215→            for year_dir in sorted(base_path.iterdir()):
   216→                if not year_dir.is_dir():
   217→                    continue
   218→                for month_dir in sorted(year_dir.iterdir()):
   219→                    if not month_dir.is_dir():
   220→                        continue
   221→                    for file_path in sorted(month_dir.glob("*.json")):
   222→                        # Extract date from filename (YYYY-MM-DD.json)
   223→                        date_str = file_path.stem
   224→                        dates.append(date_str)
   225→
   226→        return sorted(dates)
   227→
   228→    def get_latest_data(self, metric_type: str, days: int = 7) -> List[Dict[str, Any]]:
   229→        """Get the most recent data for a metric.
   230→
   231→        Args:
   232→            metric_type: Type of metric
   233→            days: Number of days to look back (default: 7)
   234→
   235→        Returns:
   236→            List of metric data dictionaries for the last N days
   237→        """
   238→        end_date = date.today()
   239→        start_date = end_date - timedelta(days=days - 1)
   240→
   241→        return self.get_metric_range(metric_type, start_date, end_date)
   242→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→from typing import List, Dict, Any, Callable
     2→from .health_read import HealthReader
     3→from .health_write import HealthWriter
     4→from .obsidian import ObsidianTool
     5→from .garmin import GarminTool
     6→from .garmin import GarminTool
     7→from .analytics import AnalyticsTool
     8→from .web import WebSearchTool
     9→from .shell_tool import execute_shell
    10→from .file_ops import download_file, save_file
    11→from .system_ops import get_system_status
    12→from .process_ops import kill_process, get_process_list
    13→
    14→# Tool Definitions
    15→TOOLS_SCHEMA = [
    16→    # --- Garmin & Metrics Read ---
    17→    {
    18→        "type": "function",
    19→        "function": {
    20→            "name": "get_daily_detailed_stats",
    21→            "description": "Get a comprehensive ONE-DAY SUMMARY (Metrics + Activity + Log Counts). Use for 'how was my day'. Do NOT use for filtering specific logs (use get_manual_history).",
    22→            "parameters": {
    23→                "type": "object",
    24→                "properties": {"target_date": {"type": "string", "description": "YYYY-MM-DD"}},
    25→                "required": ["target_date"]
    26→            }
    27→        }
    28→    },
    29→    {
    30→        "type": "function",
    31→        "function": {
    32→            "name": "get_metric_history",
    33→            "description": "**USE THIS when user asks about time-based trends**: 'past X days/weeks/months', 'recent changes', 'last year', 'how has X changed', 'X trend over time'. Returns analyzed trends with statistics for time ranges >7 days, raw data for ≤7 days. Supported metrics: rhr (resting heart rate), hrv, sleep, stress, steps, heart_rate, weight. Example: user asks 'past 2 months RHR' → call this with metric_type='rhr', start_date=60 days ago, end_date=today.",
    34→            "parameters": {
    35→                "type": "object",
    36→                "properties": {
    37→                    "metric_type": {"type": "string", "description": "Metric name: rhr, hrv, sleep, stress, steps, heart_rate, weight"},
    38→                    "start_date": {"type": "string", "description": "Start date YYYY-MM-DD (calculate from user's time range, e.g. 'past 2 months' = 60 days ago)"},
    39→                    "end_date": {"type": "string", "description": "End date YYYY-MM-DD (typically today)"}
    40→                },
    41→                "required": ["metric_type", "start_date", "end_date"]
    42→            }
    43→        }
    44→    },
    45→    {
    46→        "type": "function",
    47→        "function": {
    48→            "name": "get_activity_history",
    49→            "description": "Get list of workouts/activities in a date range.",
    50→            "parameters": {
    51→                "type": "object",
    52→                "properties": {
    53→                    "start_date": {"type": "string", "description": "YYYY-MM-DD"},
    54→                    "end_date": {"type": "string", "description": "YYYY-MM-DD"}
    55→                },
    56→                "required": ["start_date", "end_date"]
    57→            }
    58→        }
    59→    },
    60→    {
    61→        "type": "function",
    62→        "function": {
    63→            "name": "get_aggregated_analysis",
    64→            "description": "**REQUIRED for time-range questions**: When user asks 'past X days/weeks/months/years', 'recent trend', 'change over time', 'how has X changed', use THIS tool (not get_metric_history). Returns analyzed trends with statistics, yearly comparisons, and 30-day rolling averages. Examples: 'past 2 months RHR', 'HRV trend this year', 'sleep quality last 90 days'.",
    65→            "parameters": {
    66→                "type": "object",
    67→                "properties": {
    68→                    "start_date": {
    69→                        "type": "string",
    70→                        "description": "Start date YYYY-MM-DD (optional, defaults to 2 years ago)"
    71→                    },
    72→                    "end_date": {
    73→                        "type": "string",
    74→                        "description": "End date YYYY-MM-DD (optional, defaults to today)"
    75→                    },
    76→                    "metrics": {
    77→                        "type": "array",
    78→                        "items": {"type": "string"},
    79→                        "description": "Metrics to analyze: ['rhr', 'hrv', 'sleep', 'stress', 'steps']. If omitted, analyzes all."
    80→                    }
    81→                },
    82→                "required": []
    83→            }
    84→        }
    85→    },
    86→    # --- Manual Read ---
    87→    {
    88→        "type": "function",
    89→        "function": {
    90→            "name": "get_manual_history",
    91→            "description": "query manual logs (diet, alcohol, etc) over a range.",
    92→            "parameters": {
    93→                "type": "object",
    94→                "properties": {
    95→                    "start_date": {"type": "string", "description": "YYYY-MM-DD"},
    96→                    "end_date": {"type": "string", "description": "YYYY-MM-DD"},
    97→                    "category": {
    98→                        "type": "string",
    99→                        "enum": ["diet", "alcohol", "supplements", "feelings", "fasting", "all"],
   100→                        "description": "Category of logs to retrieve"
   101→                    }
   102→                },
   103→                "required": ["start_date", "end_date"]
   104→            }
   105→        }
   106→    },
   107→    # --- Manual Write (Operate) ---
   108→    {
   109→        "type": "function",
   110→        "function": {
   111→            "name": "log_diet",
   112→            "description": "Log food/meal.",
   113→            "parameters": {
   114→                "type": "object",
   115→                "properties": {
   116→                    "target_date": {"type": "string"},
   117→                    "description": {"type": "string"},
   118→                    "meal_type": {"type": "string", "enum": ["breakfast", "lunch", "dinner", "snack"]},
   119→                    "time": {"type": "string", "description": "HH:MM (optional)"}
   120→                },
   121→                "required": ["target_date", "description"]
   122→            }
   123→        }
   124→    },
   125→    {
   126→        "type": "function",
   127→        "function": {
   128→            "name": "log_alcohol",
   129→            "description": "Log alcohol.",
   130→            "parameters": {
   131→                "type": "object",
   132→                "properties": {
   133→                    "target_date": {"type": "string"},
   134→                    "alcohol_type": {"type": "string"},
   135→                    "amount": {"type": "string"},
   136→                    "time": {"type": "string", "description": "HH:MM (optional)"}
   137→                },
   138→                "required": ["target_date", "alcohol_type", "amount"]
   139→            }
   140→        }
   141→    },
   142→    {
   143→        "type": "function",
   144→        "function": {
   145→            "name": "log_supplement",
   146→            "description": "Log supplement.",
   147→            "parameters": {
   148→                "type": "object",
   149→                "properties": {
   150→                    "target_date": {"type": "string"},
   151→                    "name": {"type": "string"},
   152→                    "dosage": {"type": "string"},
   153→                    "time": {"type": "string", "description": "HH:MM (optional)"}
   154→                },
   155→                "required": ["target_date", "name", "dosage"]
   156→            }
   157→        }
   158→    },
   159→    {
   160→        "type": "function",
   161→        "function": {
   162→            "name": "log_feeling",
   163→            "description": "Log body feeling or symptom.",
   164→            "parameters": {
   165→                "type": "object",
   166→                "properties": {
   167→                    "target_date": {"type": "string"},
   168→                    "feeling_type": {"type": "string"},
   169→                    "severity": {"type": "integer", "description": "1-10"},
   170→                    "description": {"type": "string"},
   171→                    "time": {"type": "string", "description": "HH:MM (optional)"}
   172→                },
   173→                "required": ["target_date", "feeling_type", "severity", "description"]
   174→            }
   175→        }
   176→    },
   177→    {
   178→        "type": "function",
   179→        "function": {
   180→            "name": "log_fasting",
   181→            "description": "Log fasting mode.",
   182→            "parameters": {
   183→                "type": "object",
   184→                "properties": {
   185→                    "target_date": {"type": "string"},
   186→                    "mode": {"type": "string", "description": "OMAD, PSMF, etc"}
   187→                },
   188→                "required": ["target_date", "mode"]
   189→            }
   190→        }
   191→    },
   192→    # --- Sync ---
   193→    {
   194→        "type": "function",
   195→        "function": {
   196→            "name": "sync_obsidian",
   197→            "description": "Sync Obsidian note.",
   198→            "parameters": {
   199→                "type": "object",
   200→                "properties": {"target_date": {"type": "string"}},
   201→                "required": ["target_date"]
   202→            }
   203→        }
   204→    },
   205→    {
   206→        "type": "function",
   207→        "function": {
   208→            "name": "sync_garmin",
   209→            "description": "Action to synchronize the latest health data from Garmin Connect. You MUST use this tool when the user asks to 'sync', 'update', or 'fetch' data. Do not assume data is already up-to-date.",
   210→            "parameters": {
   211→                "type": "object",
   212→                "properties": {
   213→                    "target_date": {"type": "string", "description": "YYYY-MM-DD (optional, default to today)"}
   214→                },
   215→                "required": []
   216→            }
   217→        }
   218→    },
   219→    {
   220→        "type": "function",
   221→        "function": {
   222→            "name": "get_health_insights",
   223→            "description": "Get advanced health insights, correlations, and trends (e.g., alcohol impact on sleep, heart rate trends).",
   224→            "parameters": {
   225→                "type": "object",
   226→                "properties": {
   227→                    "analysis_type": {
   228→                        "type": "string",
   229→                        "description": "Type of analysis: 'recovery_correlations' (alcohol/activity impact), 'fitness_trends' (RHR/fitness), 'lifestyle_impact' (fasting/supplements)."
   230→                    },
   231→                    "lookback_days": {
   232→                        "type": "integer",
   233→                        "description": "Number of days to analyze (default 30, recommend 30-90 for trends)."
   234→                    }
   235→                },
   236→                "required": ["analysis_type"]
   237→            }
   238→        }
   239→    },
   240→    {
   241→        "type": "function",
   242→        "function": {
   243→            "name": "analyze_driver",
   244→            "description": "Analyze if a specific factor (Driver) impacts a health metric (Target). e.g., 'Does Magnesium improve sleep?'.",
   245→            "parameters": {
   246→                "type": "object",
   247→                "properties": {
   248→                    "driver_metric": {
   249→                        "type": "string",
   250→                        "description": "The cause/driver column. Examples: 'alcohol_units', 'stress', 'steps', 'fasting_mode', 'has_magnesium', 'has_alcohol'."
   251→                    },
   252→                    "target_metric": {
   253→                        "type": "string",
   254→                        "description": "The effect/target column. Examples: 'sleep', 'rhr', 'hrv', 'body_battery'."
   255→                    },
   256→                    "method": {
   257→                        "type": "string",
   258→                        "enum": ["correlation", "group_compare"],
   259→                        "description": "Use 'correlation' for continuous values (alcohol units vs sleep) or 'group_compare' for events (Magnesium YES/NO vs Sleep)."
   260→                    },
   261→                    "lookback_days": {
   262→                        "type": "integer",
   263→                        "description": "Days to analyze (default 30)."
   264→                    }
   265→                },
   266→                "required": ["driver_metric", "target_metric", "method"]
   267→            }
   268→        }
   269→    },
   270→    {
   271→        "type": "function",
   272→        "function": {
   273→            "name": "search_web",
   274→            "description": "Search the web for real-time information, latest scientific research, or biohacker experiences.",
   275→            "parameters": {
   276→                "type": "object",
   277→                "properties": {
   278→                    "query": {
   279→                        "type": "string",
   280→                        "description": "Search query (e.g. 'latest study on NAD+ effects')"
   281→                    }
   282→                },
   283→                "required": ["query"]
   284→            }
   285→        }
   286→    },
   287→    {
   288→        "type": "function",
   289→        "function": {
   290→            "name": "execute_shell",
   291→            "description": "Execute a shell command (zsh) in a persistent session. Use for system questions, file checks, or running scripts. If user asks 'how to X', providing a numbered list, you can use this to execute the user's choice.",
   292→            "parameters": {
   293→                "type": "object",
   294→                "properties": {
   295→                    "command": {
   296→                        "type": "string",
   297→                        "description": "Command to execute (e.g. 'ls -la', 'git status', 'python3 script.py')"
   298→                    },
   299→                    "timeout": {
   300→                        "type": "number",
   301→                        "description": "Timeout in seconds to wait for output (default 2.0). Increase for long-running commands like 'top' (to get a snapshot) or 'sleep 5'.",
   302→                        "default": 2.0
   303→                    }
   304→                },
   305→                "required": ["command"]
   306→            }
   307→        }
   308→    },
   309→    # --- New Shell Tools (Tier 1 & 2) ---
   310→    {
   311→        "type": "function",
   312→        "function": {
   313→            "name": "download_file",
   314→            "description": "Upload a file from the server to Slack. Use when user wants to see a log file or config.",
   315→            "parameters": {
   316→                "type": "object",
   317→                "properties": {
   318→                    "file_path": {"type": "string", "description": "Absolute path to the file"}
   319→                },
   320→                "required": ["file_path"]
   321→            }
   322→        }
   323→    },
   324→    {
   325→        "type": "function",
   326→        "function": {
   327→            "name": "save_file",
   328→            "description": "Save content to a file on the server. Use to create/edit configs.",
   329→            "parameters": {
   330→                "type": "object",
   331→                "properties": {
   332→                    "file_path": {"type": "string", "description": "Target absolute path"},
   333→                    "content": {"type": "string", "description": "Text content to save"},
   334→                    "overwrite": {"type": "boolean", "description": "Allow overwriting existing file"}
   335→                },
   336→                "required": ["file_path", "content"]
   337→            }
   338→        }
   339→    },
   340→    {
   341→        "type": "function",
   342→        "function": {
   343→            "name": "get_system_status",
   344→            "description": "Get a visual dashboard of system health (CPU, Memory, Disk, Top Processes).",
   345→            "parameters": {
   346→                "type": "object",
   347→                "properties": {},
   348→                "required": []
   349→            }
   350→        }
   351→    },
   352→    {
   353→        "type": "function",
   354→        "function": {
   355→            "name": "kill_process",
   356→            "description": "Terminate a process by PID.",
   357→            "parameters": {
   358→                "type": "object",
   359→                "properties": {
   360→                    "pid": {"type": "integer", "description": "Process ID"},
   361→                    "signal_type": {"type": "string", "enum": ["SIGTERM", "SIGKILL"], "default": "SIGTERM"}
   362→                },
   363→                "required": ["pid"]
   364→            }
   365→        }
   366→    },
   367→    {
   368→        "type": "function",
   369→        "function": {
   370→            "name": "get_process_list",
   371→            "description": "List running processes with optional name filtering.",
   372→            "parameters": {
   373→                "type": "object",
   374→                "properties": {
   375→                    "filter_name": {"type": "string", "description": "Filter by process name (case-insensitive)"},
   376→                    "limit": {"type": "integer", "default": 15}
   377→                },
   378→                "required": []
   379→            }
   380→        }
   381→    }
   382→]
   383→
   384→# Function Map
   385→TOOL_FUNCTIONS: Dict[str, Callable] = {
   386→    "get_daily_detailed_stats": HealthReader.get_daily_detailed_stats,
   387→    "get_metric_history": HealthReader.get_metric_history,
   388→    "get_activity_history": HealthReader.get_activity_history,
   389→    "get_manual_history": HealthReader.get_manual_history,
   390→    "get_aggregated_analysis": HealthReader.get_aggregated_analysis,
   391→
   392→    "log_diet": HealthWriter.log_diet,
   393→    "log_alcohol": HealthWriter.log_alcohol,
   394→    "log_supplement": HealthWriter.log_supplement,
   395→    "log_feeling": HealthWriter.log_feeling,
   396→    "log_fasting": HealthWriter.log_fasting,
   397→
   398→    "sync_obsidian": ObsidianTool.sync_obsidian,
   399→    "sync_garmin": GarminTool.sync_garmin,
   400→
   401→    "get_health_insights": AnalyticsTool.get_health_insights,
   402→    "analyze_driver": AnalyticsTool.analyze_driver,
   403→
   404→    "search_web": WebSearchTool.search_web,
   405→
   406→    "execute_shell": execute_shell,
   407→    "download_file": download_file,
   408→    "save_file": save_file,
   409→    "get_system_status": get_system_status,
   410→    "kill_process": kill_process,
   411→    "get_process_list": get_process_list
   412→}
   413→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→import json
     2→from datetime import date, datetime, timedelta
     3→from typing import Dict, Any, Optional, List
     4→from pathlib import Path
     5→
     6→from health import config
     7→from health.services.query import HealthDataQuery
     8→from health.services.manual_log_storage import ManualLogStorage, DailyManualLog
     9→from health.utils.logging_config import setup_logger
    10→
    11→logger = setup_logger(__name__)
    12→
    13→class HealthReader:
    14→    @staticmethod
    15→    def get_daily_detailed_stats(target_date: str = None, **kwargs) -> str:
    16→        """
    17→        Get all available health metrics and activities for a specific day.
    18→        Args:
    19→            target_date: ISO format date string (YYYY-MM-DD)
    20→            **kwargs: Catch-all for hallucinated arguments.
    21→        """
    22→        try:
    23→            # Handle aliases/hallucinations from LLM
    24→            if not target_date:
    25→                target_date = kwargs.get('start_date') or kwargs.get('date') or kwargs.get('day')
    26→
    27→            if not target_date:
    28→                return "Error: target_date (or start_date) is required."
    29→
    30→            d = date.fromisoformat(target_date)
    31→            query = HealthDataQuery()
    32→            data = query.get_daily_summary(d)
    33→
    34→            # Auto-sync if data is missing for recent days (today/yesterday)
    35→            # This mimics behavior of ./h.sh which usually syncs before showing
    36→            if not data.get("metrics") and (date.today() - d).days <= 1:
    37→                logger.info(f"Data missing for {target_date}, attempting auto-sync...")
    38→                try:
    39→                    from health.services.data_sync import HealthDataSync
    40→                    syncer = HealthDataSync()
    41→                    syncer.authenticate()
    42→                    syncer.sync_all_metrics(d, d, force=True)
    43→                    # Re-query after sync
    44→                    data = query.get_daily_summary(d)
    45→                    logger.info("Auto-sync completed, re-queried data.")
    46→                except Exception as sync_err:
    47→                    logger.error(f"Auto-sync failed: {sync_err}")
    48→                    # Continue with empty data, user will see "No data"
    49→
    50→
    51→            # Format summarized output to save context window
    52→            lines = [f"📊 **Health Report for {target_date}**"]
    53→
    54→            # Metrics
    55→            if data.get("metrics"):
    56→                lines.append("\n**Metrics:**")
    57→                for key, val in data["metrics"].items():
    58→                    # Flatten simple dicts for readability
    59→                    if isinstance(val, dict):
    60→                        # Extract key high-level stats if possible, otherwise stringify
    61→                        summary_str = ", ".join([f"{k}={v}" for k, v in val.items() if isinstance(v, (int, float, str))])
    62→                        if len(summary_str) > 200:
    63→                            summary_str = summary_str[:200] + "..."
    64→                        lines.append(f"- {key}: {summary_str}")
    65→                    else:
    66→                        lines.append(f"- {key}: {val}")
    67→
    68→            # Activities
    69→            if data.get("activities"):
    70→                lines.append("\n**Activities:**")
    71→                for act in data["activities"]:
    72→                    lines.append(f"- {act.get('activity_type', 'Activity')}: {act.get('duration_seconds', 0)//60} mins, {act.get('calories', 0)} kcal, Avg HR {act.get('average_heart_rate', 'N/A')}")
    73→
    74→            # Manual Logs
    75→            manual = ManualLogStorage().load_log(d)
    76→            if manual.diet_entries:
    77→                lines.append(f"\n**Diet:** {len(manual.diet_entries)} entries")
    78→            if manual.alcohol_entries:
    79→                lines.append(f"**Alcohol:** {len(manual.alcohol_entries)} entries")
    80→            if manual.fasting_mode:
    81→                lines.append(f"**Fasting:** {manual.fasting_mode}")
    82→            if manual.feeling_entries:
    83→                lines.append(f"**Feelings:** {len(manual.feeling_entries)} entries")
    84→
    85→            return "\n".join(lines) if len(lines) > 1 else f"No data found for {target_date}."
    86→
    87→        except Exception as e:
    88→            logger.error(f"Error getting detailed stats: {e}")
    89→            return f"Error retrieving data: {str(e)}"
    90→
    91→    @staticmethod
    92→    def get_metric_history(metric_type: str = None, start_date: str = None, end_date: str = None, **kwargs) -> str:
    93→        """
    94→        Get historical data for a specific metric (e.g., steps, sleep_score, heart_rate, hrv, weight).
    95→
    96→        For time ranges > 7 days, automatically uses aggregated analysis for better insights.
    97→        """
    98→        try:
    99→            # Handle alias: target_date -> start/end
   100→            if 'target_date' in kwargs and not start_date:
   101→                start_date = kwargs['target_date']
   102→                end_date = kwargs['target_date']
   103→
   104→            if not metric_type:
   105→                return "Error: Metric type is required (e.g., 'steps', 'sleep', 'heart_rate')."
   106→
   107→            if not start_date or not end_date:
   108→                return "Error: Start date and end_date are required."
   109→
   110→            start = date.fromisoformat(start_date)
   111→            end = date.fromisoformat(end_date)
   112→
   113→            # Calculate time span
   114→            time_span_days = (end - start).days + 1
   115→
   116→            # For longer time ranges (> 7 days), use aggregated analysis instead
   117→            if time_span_days > 7:
   118→                logger.info(f"Time range {time_span_days} days - using aggregated analysis")
   119→
   120→                # Map common metric name variations to our standard names
   121→                metric_mapping = {
   122→                    'heart_rate': 'rhr',
   123→                    'resting_heart_rate': 'rhr',
   124→                    'sleep_score': 'sleep',
   125→                    'sleep_quality': 'sleep',
   126→                }
   127→
   128→                metric_name = metric_mapping.get(metric_type, metric_type)
   129→
   130→                # Call aggregated analysis with single metric
   131→                return HealthReader.get_aggregated_analysis(
   132→                    start_date=start_date,
   133→                    end_date=end_date,
   134→                    metrics=[metric_name]
   135→                )
   136→
   137→            # For short time ranges (<= 7 days), return raw data as before
   138→            query = HealthDataQuery()
   139→            data_list = query.get_metric_range(metric_type, start, end)
   140→
   141→            if not data_list:
   142→                return f"No {metric_type} data found from {start_date} to {end_date}."
   143→
   144→            # Summarize list for LLM
   145→            return f"Found {len(data_list)} records for {metric_type}:\n" + str(data_list)
   146→
   147→        except Exception as e:
   148→            logger.error(f"Error getting metric history: {e}")
   149→            return f"Error retrieving {metric_type} history: {str(e)}"
   150→
   151→    @staticmethod
   152→    def get_activity_history(start_date: str = None, end_date: str = None, **kwargs) -> str:
   153→        """
   154→        Get list of activities/workouts in a date range.
   155→        """
   156→        try:
   157→            # Handle alias
   158→            if 'target_date' in kwargs and not start_date:
   159→                start_date = kwargs['target_date']
   160→                end_date = kwargs['target_date']
   161→
   162→            if not start_date or not end_date:
   163→                return "Error: Start date and end_date are required."
   164→
   165→            start = date.fromisoformat(start_date)
   166→            end = date.fromisoformat(end_date)
   167→
   168→            query = HealthDataQuery()
   169→            activities = query.get_activities_range(start, end)
   170→
   171→            if not activities:
   172→                return f"No activities found from {start_date} to {end_date}."
   173→
   174→            lines = [f"**Activities from {start_date} to {end_date}:**"]
   175→            for act in activities:
   176→                date_str = act.get('start_time', 'Unknown Date')[:10]
   177→                type_str = act.get('activity_type', 'Unknown Activity')
   178→                dist_m = act.get('distance_meters', 0)
   179→                dur_s = act.get('duration_seconds', 0)
   180→                cal = act.get('calories', 0)
   181→                lines.append(f"- {date_str}: {type_str} | {dur_s//60} mins | {cal} kcal | {dist_m/1000:.2f} km")
   182→
   183→            return "\n".join(lines)
   184→
   185→        except Exception as e:
   186→            logger.error(f"Error getting activity history: {e}")
   187→            return f"Error retrieving activity history: {str(e)}"
   188→
   189→    @staticmethod
   190→    def get_manual_history(start_date: str = None, end_date: str = None, category: str = "all", target_date: str = None, **kwargs) -> str:
   191→        """
   192→        Get manual logs (diet, alcohol, supplements, feelings, fasting) over a date range.
   193→        Args:
   194→            start_date: YYYY-MM-DD
   195→            end_date: YYYY-MM-DD
   196→            category: 'diet', 'alcohol', 'supplements', 'feelings', 'fasting', 'all'
   197→            target_date: Optional alias for single-day query (sets start=end=target)
   198→            **kwargs: Catch-all for hallucinated arguments (e.g. dosage, name) to prevent crashes.
   199→        """
   200→        try:
   201→            if kwargs:
   202→                logger.warning(f"get_manual_history received unexpected args: {kwargs}")
   203→
   204→            # Handle target_date alias for single day
   205→            if target_date:
   206→                start_date = target_date
   207→                end_date = target_date
   208→
   209→            if not start_date or not end_date:
   210→                return "Error: Please provide start_date and end_date (or target_date)."
   211→
   212→            start = date.fromisoformat(start_date)
   213→            end = date.fromisoformat(end_date)
   214→
   215→            storage = ManualLogStorage()
   216→            logs = storage.get_logs_in_range(start, end)
   217→
   218→            if not logs:
   219→                return f"No manual logs found from {start_date} to {end_date}."
   220→
   221→            lines = []
   222→            for log in logs:
   223→                date_header = f"**{log.log_date}:**"
   224→                day_lines = []
   225→
   226→                if category in ["diet", "all"] and log.diet_entries:
   227→                    day_lines.append(f"  Diet: {len(log.diet_entries)} items")
   228→                    for e in log.diet_entries:
   229→                        day_lines.append(f"    - [{e.time}] {e.description} ({e.meal_type})")
   230→
   231→                if category in ["alcohol", "all"] and log.alcohol_entries:
   232→                    day_lines.append(f"  Alcohol: {len(log.alcohol_entries)} entries")
   233→                    for e in log.alcohol_entries:
   234→                        day_lines.append(f"    - [{e.time}] {e.amount} {e.alcohol_type}")
   235→
   236→                if category in ["supplements", "all"] and log.supplement_entries:
   237→                    day_lines.append(f"  Supplements: {len(log.supplement_entries)} entries")
   238→                    for e in log.supplement_entries:
   239→                        day_lines.append(f"    - [{e.time}] {e.supplement_name} {e.dosage}")
   240→
   241→                if category in ["feelings", "all"] and log.feeling_entries:
   242→                    day_lines.append(f"  Feelings: {len(log.feeling_entries)} entries")
   243→                    for e in log.feeling_entries:
   244→                        day_lines.append(f"    - [{e.time}] {e.feeling_type} ({e.severity}/10): {e.description}")
   245→
   246→                if category in ["fasting", "all"] and log.fasting_mode:
   247→                    day_lines.append(f"  Fasting: {log.fasting_mode}")
   248→
   249→                if day_lines:
   250→                    lines.append(date_header)
   251→                    lines.extend(day_lines)
   252→
   253→            return "\n".join(lines) if lines else f"No manual data for '{category}' found in range."
   254→
   255→        except Exception as e:
   256→            logger.error(f"Error getting manual history: {e}")
   257→            return f"Error retrieving manual history: {str(e)}"
   258→
   259→    @staticmethod
   260→    def get_aggregated_analysis(
   261→        start_date: str = None,
   262→        end_date: str = None,
   263→        metrics: List[str] = None,
   264→        **kwargs
   265→    ) -> str:
   266→        """
   267→        Get comprehensive health trend analysis over a large time window.
   268→
   269→        This method aggregates data and calculates statistics to minimize AI tool calls.
   270→        Instead of returning raw data, it provides a pre-analyzed report with trends,
   271→        averages, and comparisons.
   272→
   273→        Args:
   274→            start_date: Start date in YYYY-MM-DD format
   275→            end_date: End date in YYYY-MM-DD format
   276→            metrics: List of metrics to analyze. If None, analyzes common metrics:
   277→                    ['resting_heart_rate', 'hrv', 'sleep', 'stress', 'steps']
   278→            **kwargs: Catch-all for hallucinated arguments
   279→
   280→        Returns:
   281→            Formatted analysis report with trends, statistics, and comparisons
   282→        """
   283→        try:
   284→            # Default to last 2 years if not specified
   285→            if not end_date:
   286→                end_date = date.today().isoformat()
   287→            if not start_date:
   288→                start = date.fromisoformat(end_date) - timedelta(days=730)
   289→                start_date = start.isoformat()
   290→
   291→            start = date.fromisoformat(start_date)
   292→            end = date.fromisoformat(end_date)
   293→
   294→            # Default metrics if not specified (use correct metric names from config)
   295→            if not metrics:
   296→                metrics = ['rhr', 'hrv', 'sleep', 'stress', 'steps']
   297→
   298→            query = HealthDataQuery()
   299→            lines = [f"📊 **Health Trend Analysis ({start_date} to {end_date})**"]
   300→            lines.append(f"Time span: {(end - start).days} days\n")
   301→
   302→            # Analyze each metric
   303→            for metric in metrics:
   304→                try:
   305→                    data_list = query.get_metric_range(metric, start, end)
   306→
   307→                    if not data_list:
   308→                        lines.append(f"\n❌ **{metric.upper()}**: No data available")
   309→                        continue
   310→
   311→                    lines.append(f"\n{'='*60}")
   312→                    lines.append(f"📈 **{metric.upper().replace('_', ' ')}**")
   313→                    lines.append(f"{'='*60}")
   314→
   315→                    # Metric-specific analysis
   316→                    if metric in ['resting_heart_rate', 'rhr']:
   317→                        lines.extend(_analyze_resting_heart_rate(data_list, start, end))
   318→                    elif metric == 'hrv':
   319→                        lines.extend(_analyze_hrv(data_list, start, end))
   320→                    elif metric == 'sleep':
   321→                        lines.extend(_analyze_sleep(data_list, start, end))
   322→                    elif metric == 'stress':
   323→                        lines.extend(_analyze_stress(data_list, start, end))
   324→                    elif metric == 'steps':
   325→                        lines.extend(_analyze_steps(data_list, start, end))
   326→                    else:
   327→                        # Generic analysis for unknown metrics
   328→                        lines.extend(_analyze_generic(metric, data_list, start, end))
   329→
   330→                except Exception as metric_err:
   331→                    logger.error(f"Error analyzing {metric}: {metric_err}")
   332→                    lines.append(f"\n❌ **{metric}**: Error - {str(metric_err)}")
   333→
   334→            return "\n".join(lines)
   335→
   336→        except Exception as e:
   337→            logger.error(f"Error in aggregated analysis: {e}")
   338→            return f"Error generating analysis: {str(e)}"
   339→
   340→
   341→def _analyze_resting_heart_rate(data_list: List[Dict], start: date, end: date) -> List[str]:
   342→    """Analyze resting heart rate trends."""
   343→    lines = []
   344→
   345→    # Extract values (try both 'rhr' and 'resting_heart_rate' field names)
   346→    values = []
   347→    dates = []
   348→    for item in data_list:
   349→        rhr_val = item.get('rhr') or item.get('resting_heart_rate')
   350→        if rhr_val:
   351→            values.append(rhr_val)
   352→            dates.append(item.get('date', ''))
   353→
   354→    if not values:
   355→        return ["No valid resting heart rate data"]
   356→
   357→    # Overall statistics
   358→    avg_all = sum(values) / len(values)
   359→    min_val = min(values)
   360→    max_val = max(values)
   361→
   362→    lines.append(f"📊 Overall: {avg_all:.1f} bpm (range: {min_val}-{max_val})")
   363→    lines.append(f"   Data points: {len(values)} days")
   364→
   365→    # Split by year for trend analysis
   366→    yearly_avgs = {}
   367→    for item in data_list:
   368→        rhr_val = item.get('rhr') or item.get('resting_heart_rate')
   369→        if 'date' in item and rhr_val:
   370→            year = item['date'][:4]
   371→            if year not in yearly_avgs:
   372→                yearly_avgs[year] = []
   373→            yearly_avgs[year].append(rhr_val)
   374→
   375→    if len(yearly_avgs) > 1:
   376→        lines.append("\n📅 Yearly Trends:")
   377→        prev_avg = None
   378→        for year in sorted(yearly_avgs.keys()):
   379→            year_avg = sum(yearly_avgs[year]) / len(yearly_avgs[year])
   380→            change_str = ""
   381→            if prev_avg:
   382→                change = year_avg - prev_avg
   383→                pct = (change / prev_avg) * 100
   384→                direction = "↓" if change < 0 else "↑"
   385→                change_str = f" ({direction} {abs(change):.1f} bpm, {abs(pct):.1f}%)"
   386→            lines.append(f"   {year}: {year_avg:.1f} bpm{change_str}")
   387→            prev_avg = year_avg
   388→
   389→    # Recent 30 days vs baseline
   390→    recent_30 = [v for i, v in enumerate(values) if len(values) - i <= 30]
   391→    if len(recent_30) >= 5:
   392→        recent_avg = sum(recent_30) / len(recent_30)
   393→        baseline = sum(values[:len(values)//2]) / (len(values)//2) if len(values) > 60 else avg_all
   394→        change = recent_avg - baseline
   395→        status = "✅ Improved" if change < -2 else "⚠ Elevated" if change > 2 else "🔄 Stable"
   396→        lines.append(f"\n🎯 Recent 30 days: {recent_avg:.1f} bpm ({status})")
   397→
   398→    return lines
   399→
   400→
   401→def _analyze_hrv(data_list: List[Dict], start: date, end: date) -> List[str]:
   402→    """Analyze HRV (Heart Rate Variability) trends."""
   403→    lines = []
   404→
   405→    # Extract HRV values (can be stored in multiple field formats)
   406→    values = []
   407→    weekly_avgs = []
   408→    statuses = []
   409→
   410→    for item in data_list:
   411→        hrv_val = None
   412→
   413→        # Try different field names (prioritize hrv_value which is the main field)
   414→        if 'hrv_value' in item:
   415→            hrv_val = item['hrv_value']
   416→        elif 'average_hrv_rmssd' in item:
   417→            hrv_val = item['average_hrv_rmssd']
   418→        elif 'hrv_rmssd' in item:
   419→            hrv_val = item['hrv_rmssd']
   420→        elif 'lastNightAvg' in item:
   421→            hrv_val = item['lastNightAvg']
   422→
   423→        # Also try to extract from raw_data nested structure
   424→        if not hrv_val and 'raw_data' in item and isinstance(item['raw_data'], dict):
   425→            hrv_summary = item['raw_data'].get('hrvSummary', {})
   426→            hrv_val = hrv_summary.get('lastNightAvg')
   427→
   428→            # Collect weekly average if available
   429→            weekly = hrv_summary.get('weeklyAvg')
   430→            if weekly and weekly > 0:
   431→                weekly_avgs.append(weekly)
   432→
   433→        if hrv_val and hrv_val > 0:
   434→            values.append(hrv_val)
   435→
   436→            # Track balance status if available
   437→            status = item.get('status') or item.get('hrv_status')
   438→            if status:
   439→                statuses.append(status)
   440→
   441→    if not values:
   442→        return ["No valid HRV data"]
   443→
   444→    avg_all = sum(values) / len(values)
   445→    lines.append(f"📊 Overall: {avg_all:.1f} ms (last night average)")
   446→    lines.append(f"   Data points: {len(values)} days")
   447→
   448→    # Show weekly average if available
   449→    if weekly_avgs:
   450→        avg_weekly = sum(weekly_avgs) / len(weekly_avgs)
   451→        lines.append(f"   Weekly Average: {avg_weekly:.1f} ms")
   452→
   453→    # Yearly trend
   454→    yearly_avgs = {}
   455→    for item in data_list:
   456→        # Extract HRV value using same logic as above
   457→        hrv_val = item.get('hrv_value')
   458→        if not hrv_val and 'raw_data' in item:
   459→            hrv_summary = item['raw_data'].get('hrvSummary', {})
   460→            hrv_val = hrv_summary.get('lastNightAvg')
   461→
   462→        if 'date' in item and hrv_val and hrv_val > 0:
   463→            year = item['date'][:4]
   464→            if year not in yearly_avgs:
   465→                yearly_avgs[year] = []
   466→            yearly_avgs[year].append(hrv_val)
   467→
   468→    if len(yearly_avgs) > 1:
   469→        lines.append("\n📅 Yearly Trends:")
   470→        prev_avg = None
   471→        for year in sorted(yearly_avgs.keys()):
   472→            year_avg = sum(yearly_avgs[year]) / len(yearly_avgs[year])
   473→            change_str = ""
   474→            if prev_avg:
   475→                change = year_avg - prev_avg
   476→                pct = (change / prev_avg) * 100
   477→                direction = "↑" if change > 0 else "↓"
   478→                status = "✅" if change > 0 else "⚠"
   479→                change_str = f" ({status} {direction} {abs(change):.1f} ms, {abs(pct):.1f}%)"
   480→            lines.append(f"   {year}: {year_avg:.1f} ms{change_str}")
   481→            prev_avg = year_avg
   482→
   483→    # Balance status summary
   484→    if statuses:
   485→        balanced_count = statuses.count('BALANCED')
   486→        unbalanced_count = statuses.count('UNBALANCED')
   487→        total = len(statuses)
   488→        lines.append(f"\n🎯 Recovery Status: {balanced_count}/{total} days BALANCED ({balanced_count/total*100:.0f}%)")
   489→
   490→    # Recent trend
   491→    recent_30 = values[-30:] if len(values) >= 30 else values
   492→    if len(recent_30) >= 5:
   493→        recent_avg = sum(recent_30) / len(recent_30)
   494→        baseline = sum(values[:len(values)//2]) / (len(values)//2) if len(values) > 60 else avg_all
   495→        change = recent_avg - baseline
   496→        status = "✅ Improved" if change > 3 else "⚠ Declined" if change < -3 else "🔄 Stable"
   497→        lines.append(f"   Recent 30 days: {recent_avg:.1f} ms ({status})")
   498→
   499→    return lines
   500→
   501→
   502→def _analyze_sleep(data_list: List[Dict], start: date, end: date) -> List[str]:
   503→    """Analyze sleep quality trends."""
   504→    lines = []
   505→
   506→    total_sleep_hrs = []
   507→    deep_sleep_hrs = []
   508→    rem_sleep_hrs = []
   509→    sleep_scores = []
   510→
   511→    for item in data_list:
   512→        # Total sleep
   513→        total = item.get('total_sleep_seconds', 0) / 3600
   514→        if total > 0:
   515→            total_sleep_hrs.append(total)
   516→
   517→        # Deep sleep
   518→        deep = item.get('deep_sleep_seconds', 0) / 3600
   519→        if deep > 0:
   520→            deep_sleep_hrs.append(deep)
   521→
   522→        # REM sleep
   523→        rem = item.get('rem_sleep_seconds', 0) / 3600
   524→        if rem > 0:
   525→            rem_sleep_hrs.append(rem)
   526→
   527→        # Sleep score
   528→        score = item.get('overall_sleep_score') or item.get('sleep_score', 0)
   529→        if score > 0:
   530→            sleep_scores.append(score)
   531→
   532→    if not total_sleep_hrs:
   533→        return ["No valid sleep data"]
   534→
   535→    avg_total = sum(total_sleep_hrs) / len(total_sleep_hrs)
   536→    avg_deep = sum(deep_sleep_hrs) / len(deep_sleep_hrs) if deep_sleep_hrs else 0
   537→    avg_rem = sum(rem_sleep_hrs) / len(rem_sleep_hrs) if rem_sleep_hrs else 0
   538→    avg_score = sum(sleep_scores) / len(sleep_scores) if sleep_scores else 0
   539→
   540→    lines.append(f"📊 Overall Averages:")
   541→    lines.append(f"   Total Sleep: {avg_total:.1f} hrs/night")
   542→    if avg_deep > 0:
   543→        deep_pct = (avg_deep / avg_total) * 100
   544→        lines.append(f"   Deep Sleep: {avg_deep:.1f} hrs ({deep_pct:.0f}%)")
   545→    if avg_rem > 0:
   546→        rem_pct = (avg_rem / avg_total) * 100
   547→        lines.append(f"   REM Sleep: {avg_rem:.1f} hrs ({rem_pct:.0f}%)")
   548→    if avg_score > 0:
   549→        lines.append(f"   Sleep Score: {avg_score:.0f}/100")
   550→
   551→    # Recent 30 days comparison
   552→    recent_30_total = total_sleep_hrs[-30:] if len(total_sleep_hrs) >= 30 else total_sleep_hrs
   553→    if len(recent_30_total) >= 5:
   554→        recent_avg = sum(recent_30_total) / len(recent_30_total)
   555→        change = recent_avg - avg_total
   556→        status = "✅" if abs(change) < 0.3 else "⚠"
   557→        lines.append(f"\n🎯 Recent 30 days: {recent_avg:.1f} hrs ({status})")
   558→
   559→    return lines
   560→
   561→
   562→def _analyze_stress(data_list: List[Dict], start: date, end: date) -> List[str]:
   563→    """Analyze stress level trends."""
   564→    lines = []
   565→
   566→    avg_stress_values = []
   567→    high_stress_days = 0
   568→
   569→    for item in data_list:
   570→        avg_stress = item.get('average_stress_level')
   571→        if avg_stress is not None:
   572→            avg_stress_values.append(avg_stress)
   573→            if avg_stress > 50:
   574→                high_stress_days += 1
   575→
   576→    if not avg_stress_values:
   577→        return ["No valid stress data"]
   578→
   579→    avg_all = sum(avg_stress_values) / len(avg_stress_values)
   580→    lines.append(f"📊 Average Stress: {avg_all:.1f}/100")
   581→    lines.append(f"   High stress days (>50): {high_stress_days}/{len(avg_stress_values)} ({high_stress_days/len(avg_stress_values)*100:.0f}%)")
   582→
   583→    # Recent trend
   584→    recent_30 = avg_stress_values[-30:] if len(avg_stress_values) >= 30 else avg_stress_values
   585→    if len(recent_30) >= 5:
   586→        recent_avg = sum(recent_30) / len(recent_30)
   587→        change = recent_avg - avg_all
   588→        status = "✅ Lower" if change < -5 else "⚠ Higher" if change > 5 else "🔄 Stable"
   589→        lines.append(f"\n🎯 Recent 30 days: {recent_avg:.1f}/100 ({status})")
   590→
   591→    return lines
   592→
   593→
   594→def _analyze_steps(data_list: List[Dict], start: date, end: date) -> List[str]:
   595→    """Analyze daily steps trends."""
   596→    lines = []
   597→
   598→    step_values = []
   599→    for item in data_list:
   600→        steps = item.get('total_steps')
   601→        if steps:
   602→            step_values.append(steps)
   603→
   604→    if not step_values:
   605→        return ["No valid steps data"]
   606→
   607→    avg_all = sum(step_values) / len(step_values)
   608→    lines.append(f"📊 Average Daily Steps: {avg_all:,.0f}")
   609→
   610→    # Active days (>5000 steps)
   611→    active_days = sum(1 for s in step_values if s > 5000)
   612→    lines.append(f"   Active days (>5k steps): {active_days}/{len(step_values)} ({active_days/len(step_values)*100:.0f}%)")
   613→
   614→    # Recent trend
   615→    recent_30 = step_values[-30:] if len(step_values) >= 30 else step_values
   616→    if len(recent_30) >= 5:
   617→        recent_avg = sum(recent_30) / len(recent_30)
   618→        change = recent_avg - avg_all
   619→        pct = (change / avg_all) * 100
   620→        status = "✅" if change > 500 else "⚠" if change < -500 else "🔄"
   621→        lines.append(f"\n🎯 Recent 30 days: {recent_avg:,.0f} steps ({status} {pct:+.0f}%)")
   622→
   623→    return lines
   624→
   625→
   626→def _analyze_generic(metric: str, data_list: List[Dict], start: date, end: date) -> List[str]:
   627→    """Generic analysis for unknown metric types."""
   628→    lines = [f"Data points: {len(data_list)}"]
   629→    lines.append("(Use specific metrics like 'resting_heart_rate', 'hrv', 'sleep' for detailed analysis)")
   630→    return lines
   631→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now let me examine the analytics engine and weekly report files:


     1→import pandas as pd
     2→import numpy as np
     3→from datetime import date, timedelta
     4→from typing import Dict, Any, List, Optional
     5→from health.services.query import HealthDataQuery
     6→from health.services.manual_log_storage import ManualLogStorage
     7→from health.utils.logging_config import setup_logger
     8→
     9→logger = setup_logger(__name__)
    10→
    11→class HealthAnalyst:
    12→    """
    13→    Advanced analytics engine for health data using Pandas.
    14→    Calculates correlations, trends, and lifestyle impacts.
    15→    """
    16→
    17→    def __init__(self):
    18→        self.query = HealthDataQuery()
    19→        self.manual_storage = ManualLogStorage()
    20→
    21→    def get_dataframe(self, days: int = 30) -> pd.DataFrame:
    22→        """
    23→        Fetch all relevant data for the last N days and merge into a single DataFrame.
    24→        """
    25→        end_date = date.today()
    26→        start_date = end_date - timedelta(days=days)
    27→
    28→        # 1. Fetch Daily Metrics (Steps, Sleep, HR, Stess, Body Battery)
    29→        # We need to fetch each metric range and merge by date
    30→        metrics_to_fetch = ["sleep", "heart_rate", "stress", "body_battery", "steps", "hrv", "rhr"]
    31→
    32→        data_map = {} # date -> dict of metrics
    33→
    34→        # Initialize dates
    35→        curr = start_date
    36→        while curr <= end_date:
    37→            data_map[curr.isoformat()] = {"date": curr.isoformat()}
    38→            curr += timedelta(days=1)
    39→
    40→        # Batch fetch metrics (this might be slow if we do it one by one,
    41→        # but HealthDataQuery is built for this. Ideally we optimize query later)
    42→        for metric in metrics_to_fetch:
    43→            points = self.query.get_metric_range(metric, start_date, end_date)
    44→            for p in points:
    45→                # Key extraction logic similar to Reader
    46→                # Simplified extraction for DataFrame
    47→                val = np.nan
    48→                if hasattr(p, 'value'): val = p.value
    49→                elif isinstance(p, dict):
    50→                    # Try common keys
    51→                    if 'average_heart_rate' in p: val = p['average_heart_rate']
    52→                    elif 'overall_sleep_score' in p: val = p['overall_sleep_score']
    53→                    elif 'resting_heart_rate' in p: val = p['resting_heart_rate']
    54→                    elif 'average_stress_level' in p: val = p['average_stress_level']
    55→                    elif 'charged' in p: val = p.get('charged', 0) # Body battery charged?
    56→                    # Actually for body battery we usually want max or charged
    57→
    58→                # Store in map
    59→                d_str = p.get('calendar_date') or p.get('date')
    60→                if d_str and d_str in data_map:
    61→                    data_map[d_str][metric] = val
    62→
    63→        # 2. Fetch Manual Logs (Alcohol, Fasting, Supplements)
    64→        logs = self.manual_storage.get_logs_in_range(start_date, end_date)
    65→        for log in logs:
    66→            d_str = log.log_date
    67→            if d_str in data_map:
    68→                # Alcohol (Boolean & Amount)
    69→                data_map[d_str]["alcohol_units"] = sum([1 for _ in log.alcohol_entries])
    70→                data_map[d_str]["has_alcohol"] = 1 if log.alcohol_entries else 0
    71→
    72→                # Fasting (Categorical)
    73→                data_map[d_str]["fasting_mode"] = log.fasting_mode or "Normal"
    74→
    75→                # Supplements (One-hotish checks)
    76→                data_map[d_str]["has_magnesium"] = 1 if any("magnesium" in s.supplement_name.lower() for s in log.supplement_entries) else 0
    77→
    78→        # Convert to DataFrame
    79→        df = pd.DataFrame(list(data_map.values()))
    80→        df['date'] = pd.to_datetime(df['date'])
    81→        df = df.sort_values('date').set_index('date')
    82→        return df
    83→
    84→    def analyze_recovery_correlations(self, days: int = 90) -> Dict[str, Any]:
    85→        """
    86→        Analyze impact of Alcohol and Activity on Next-Day Recovery (Sleep, HRV, RHR).
    87→        """
    88→        df = self.get_dataframe(days)
    89→
    90→        if df.empty:
    91→            return {"error": "No data available"}
    92→
    93→        # Shift recovery metrics to represent "Next Day"
    94→        # We want to see if Alcohol on Day T affects Sleep on Day T (technically sleep starts on Day T night)
    95→        # Actually usually Alcohol on Day T affects Sleep Score of the night of Day T (which serves Day T+1 morning)
    96→        # Garmin assigns Sleep Date to the morning it ends.
    97→        # So Alcohol on Jan 1 affects Sleep of Jan 2.
    98→
    99→        # Let's align:
   100→        # Feature: Alcohol(T)
   101→        # Target: Sleep(T+1), RHR(T+1)
   102→
   103→        df['next_sleep'] = df['sleep'].shift(-1)
   104→        df['next_rhr'] = df['rhr'].shift(-1)
   105→        df['next_hrv'] = df['hrv'].shift(-1)
   106→
   107→        results = {}
   108→
   109→        # 1. Alcohol Impact
   110→        alcohol_days = df[df['has_alcohol'] == 1]
   111→        sober_days = df[df['has_alcohol'] == 0]
   112→
   113→        if len(alcohol_days) > 0 and len(sober_days) > 0:
   114→            results['alcohol_impact'] = {
   115→                "sample_size_alcohol": len(alcohol_days),
   116→                "sample_size_sober": len(sober_days),
   117→                "avg_sleep_alcohol": float(alcohol_days['next_sleep'].mean()),
   118→                "avg_sleep_sober": float(sober_days['next_sleep'].mean()),
   119→                "avg_rhr_alcohol": float(alcohol_days['next_rhr'].mean()),
   120→                "avg_rhr_sober": float(sober_days['next_rhr'].mean()),
   121→                "sleep_diff": float(alcohol_days['next_sleep'].mean() - sober_days['next_sleep'].mean())
   122→            }
   123→
   124→        return results
   125→
   126→    def analyze_fitness_trends(self, days: int = 90) -> Dict[str, Any]:
   127→        """
   128→        Analyze RHR vs Activity trends.
   129→        """
   130→        df = self.get_dataframe(days)
   131→        if df.empty: return {"error": "No data"}
   132→
   133→        # Calculate rolling averages (7-day)
   134→        df['rhr_roll'] = df['rhr'].rolling(7).mean()
   135→        df['steps_roll'] = df['steps'].rolling(7).mean()
   136→
   137→        # Simple Linear Trend
   138→        # Drop NaNs
   139→        valid = df.dropna(subset=['rhr'])
   140→        if len(valid) > 10:
   141→            # Simple slope (not robust but indicative)
   142→            # rhr change over period
   143→            start_rhr = valid.iloc[0]['rhr_roll'] if not np.isnan(valid.iloc[0]['rhr_roll']) else valid.iloc[0]['rhr']
   144→            end_rhr = valid.iloc[-1]['rhr_roll'] if not np.isnan(valid.iloc[-1]['rhr_roll']) else valid.iloc[-1]['rhr']
   145→
   146→            return {
   147→                "period_days": days,
   148→                "start_rhr_7d_avg": float(start_rhr),
   149→                "end_rhr_7d_avg": float(end_rhr),
   150→                "trend": "improving" if end_rhr < start_rhr else "declining"
   151→            }
   152→
   153→        return {"msg": "Not enough data for trend analysis"}
   154→
   155→    def analyze_lifestyle_impact(self, days: int = 30) -> Dict[str, Any]:
   156→        """
   157→        Analyze Fasting and Supplements.
   158→        """
   159→        df = self.get_dataframe(days)
   160→        if df.empty: return {"error": "No data"}
   161→
   162→        results = {}
   163→
   164→        # Fasting Impact
   165→        # Compare "Normal" vs other modes
   166→        fasting_stats = df.groupby('fasting_mode')['sleep'].mean().to_dict()
   167→        results['fasting_sleep_scores'] = fasting_stats
   168→
   169→        return results
   170→
   171→    def compare_groups(self, condition_col: str, target_col: str, days: int = 90) -> Dict[str, Any]:
   172→        """
   173→        Compare average of 'target_col' when 'condition_col' is non-zero vs zero.
   174→        Example: Sleep Score when has_magnesium=1 vs 0.
   175→        """
   176→        df = self.get_dataframe(days)
   177→        if df.empty: return {"error": "No data"}
   178→
   179→        if condition_col not in df.columns or target_col not in df.columns:
   180→            return {"error": f"Columns not found: {condition_col} or {target_col}"}
   181→
   182→        group_true = df[df[condition_col] > 0][target_col]
   183→        group_false = df[df[condition_col] == 0][target_col]
   184→
   185→        if len(group_true) == 0 or len(group_false) == 0:
   186→            return {"error": "One group has no data (e.g. never took supplement)"}
   187→
   188→        avg_true = group_true.mean()
   189→        avg_false = group_false.mean()
   190→        diff_pct = ((avg_true - avg_false) / avg_false) * 100 if avg_false != 0 else 0
   191→
   192→        return {
   193→            "condition": condition_col,
   194→            "target": target_col,
   195→            "avg_with_condition": float(avg_true),
   196→            "avg_without_condition": float(avg_false),
   197→            "sample_with": len(group_true),
   198→            "sample_without": len(group_false),
   199→            "difference_pct": float(diff_pct),
   200→            "verdict": "better" if avg_true > avg_false else "worse"
   201→        }
   202→    def analyze_lagged_correlation(self, driver: str, target: str, lag: int = 1, days: int = 90) -> Dict[str, Any]:
   203→        """
   204→        Analyze if 'driver' on Day T correlates with 'target' on Day T+lag.
   205→        Example: Alcohol (T) -> Sleep Score (T+1).
   206→
   207→        Args:
   208→            driver: Column name of the driver (e.g., 'alcohol_units', 'stress').
   209→            target: Column name of the target (e.g., 'sleep', 'hrv', 'rhr').
   210→            lag: Days to shift target (1 means next day).
   211→            days: Lookback window.
   212→        """
   213→        df = self.get_dataframe(days)
   214→        if df.empty: return {"error": "No data"}
   215→
   216→        # Verify columns exist
   217→        if driver not in df.columns or target not in df.columns:
   218→            return {"error": f"Columns not found: {driver} or {target}"}
   219→
   220→        # Shift target
   221→        # lag=1 means we align Driver(T) with Target(T+1)
   222→        target_col = f"target_lag_{lag}"
   223→        df[target_col] = df[target].shift(-lag)
   224→
   225→        # Filter valid rows
   226→        valid = df.dropna(subset=[driver, target_col])
   227→        if len(valid) < 5:
   228→            return {"error": "Not enough data points (<5)"}
   229→
   230→        # Calculate Correlation
   231→        corr = valid[driver].corr(valid[target_col])
   232→
   233→        return {
   234→            "driver": driver,
   235→            "target": target,
   236→            "lag_days": lag,
   237→            "correlation": float(corr) if not pd.isna(corr) else 0.0,
   238→            "sample_size": len(valid),
   239→            "msg": f" Correlation {corr:.2f} (1.0 is perfect positive, -1.0 is perfect negative)"
   240→        }
   241→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→"""Weekly health report generator.
     2→
     3→Generates a Slack-formatted weekly health summary covering sleep, steps,
     4→recovery, body battery, and lifestyle data.
     5→"""
     6→
     7→from datetime import date, timedelta
     8→from typing import Any, Optional
     9→
    10→from health.services.manual_log_storage import ManualLogStorage
    11→from health.services.query import HealthDataQuery
    12→from health.utils.logging_config import setup_logger
    13→
    14→logger = setup_logger(__name__)
    15→
    16→
    17→class WeeklyReportGenerator:
    18→    """Generates weekly health reports from stored Garmin and manual log data."""
    19→
    20→    def __init__(self) -> None:
    21→        self.query = HealthDataQuery()
    22→        self.manual_storage = ManualLogStorage()
    23→
    24→    def generate(self, week_start: date) -> str:
    25→        """Generate a Slack-formatted weekly health report.
    26→
    27→        Args:
    28→            week_start: The Monday that starts the report week.
    29→
    30→        Returns:
    31→            Formatted report string ready for Slack posting.
    32→        """
    33→        week_end = week_start + timedelta(days=6)
    34→        prev_week_start = week_start - timedelta(days=7)
    35→        prev_week_end = week_start - timedelta(days=1)
    36→
    37→        logger.info(f"Generating weekly report for {week_start} ~ {week_end}")
    38→
    39→        sleep_stats = self._get_sleep_stats(week_start, week_end)
    40→        steps_stats = self._get_steps_stats(week_start, week_end)
    41→        activity_stats = self._get_activity_stats(week_start, week_end)
    42→        intensity_stats = self._get_intensity_stats(week_start, week_end)
    43→        recovery_stats = self._get_recovery_stats(week_start, week_end)
    44→        prev_recovery = self._get_recovery_stats(prev_week_start, prev_week_end)
    45→        battery_stats = self._get_battery_stats(week_start, week_end)
    46→        lifestyle_stats = self._get_lifestyle_stats(week_start, week_end)
    47→
    48→        all_stats = {
    49→            "sleep": sleep_stats,
    50→            "steps": steps_stats,
    51→            "activity": activity_stats,
    52→            "intensity": intensity_stats,
    53→            "recovery": recovery_stats,
    54→            "battery": battery_stats,
    55→            "lifestyle": lifestyle_stats,
    56→        }
    57→        highlight = self._pick_highlight(all_stats)
    58→
    59→        lines: list[str] = []
    60→
    61→        # 1. Title
    62→        lines.append(f"📊 *健康周报 {week_start} ~ {week_end}*")
    63→        lines.append("")
    64→
    65→        # 2. Sleep
    66→        lines.append("😴 *睡眠*")
    67→        if sleep_stats.get("count", 0) > 0:
    68→            lines.append(f"  • 平均得分：{sleep_stats['avg_score']:.0f} 分")
    69→            lines.append(
    70→                f"  • 平均时长：{self._format_duration(sleep_stats['avg_duration_sec'])}"
    71→            )
    72→            if sleep_stats.get("best_date"):
    73→                lines.append(
    74→                    f"  • 最佳：{sleep_stats['best_date']} ({sleep_stats['best_score']:.0f} 分)"
    75→                )
    76→            if sleep_stats.get("worst_date"):
    77→                lines.append(
    78→                    f"  • 最差：{sleep_stats['worst_date']} ({sleep_stats['worst_score']:.0f} 分)"
    79→                )
    80→        else:
    81→            lines.append("  • 本周无睡眠数据")
    82→        lines.append("")
    83→
    84→        # 3. Steps & Activity
    85→        act_count = activity_stats.get("count", 0)
    86→        lines.append(f"🏃 *运动*{'（共 ' + str(act_count) + ' 次）' if act_count else ''}")
    87→
    88→        # Steps line
    89→        if steps_stats.get("count", 0) > 0:
    90→            lines.append(
    91→                f"  • 步数达标（≥10000）：{steps_stats['goal_days']} 天"
    92→                f" | 平均 {steps_stats['avg_steps']:,.0f} 步"
    93→            )
    94→        else:
    95→            lines.append("  • 本周无步数数据")
    96→
    97→        # Intensity minutes line
    98→        if intensity_stats.get("has_data"):
    99→            lines.append(
   100→                f"  • 强度分钟：中等 {intensity_stats['moderate_min']}min"
   101→                f" + 剧烈 {intensity_stats['vigorous_min']}min"
   102→                f" = {intensity_stats['total_min']} min"
   103→                f"（WHO 目标 150 min，完成 {intensity_stats['goal_pct']}%）"
   104→            )
   105→
   106→        # HR zones
   107→        zone_seconds = activity_stats.get("zone_seconds", [0, 0, 0, 0, 0])
   108→        if any(s > 0 for s in zone_seconds):
   109→            zone_parts = "  ".join(
   110→                f"Z{i + 1} {self._format_duration(zone_seconds[i])}"
   111→                for i in range(5)
   112→            )
   113→            lines.append("")
   114→            lines.append("  *心率区间（全周累计）*")
   115→            lines.append(f"  • {zone_parts}")
   116→
   117→        # Activity detail list
   118→        activities = activity_stats.get("activities", [])
   119→        if activities:
   120→            lines.append("")
   121→            lines.append("  *运动明细*")
   122→            for act in activities:
   123→                hr_part = ""
   124→                if act["avg_hr"] or act["max_hr"]:
   125→                    hr_part = f" | ♥ {act['avg_hr']}/{act['max_hr']} bpm"
   126→                cal_part = f" | {act['calories']}kcal" if act["calories"] else ""
   127→                lines.append(
   128→                    f"  • {act['date']}  {act['name']}"
   129→                    f"  {act['duration_min']}min{cal_part}{hr_part}"
   130→                )
   131→        lines.append("")
   132→
   133→        # 4. Recovery
   134→        lines.append("❤ *恢复*")
   135→        if recovery_stats.get("count", 0) > 0:
   136→            hrv_line = f"  • 平均 HRV：{recovery_stats['avg_hrv']:.0f} ms"
   137→            if prev_recovery.get("count", 0) > 0:
   138→                diff = recovery_stats["avg_hrv"] - prev_recovery["avg_hrv"]
   139→                sign = "+" if diff >= 0 else ""
   140→                hrv_line += f"（{sign}{diff:.0f} vs 上周）"
   141→            lines.append(hrv_line)
   142→
   143→            rhr_line = f"  • 平均静息心率：{recovery_stats['avg_rhr']:.0f} bpm"
   144→            if prev_recovery.get("count", 0) > 0:
   145→                diff = recovery_stats["avg_rhr"] - prev_recovery["avg_rhr"]
   146→                sign = "+" if diff >= 0 else ""
   147→                rhr_line += f"（{sign}{diff:.0f} vs 上周）"
   148→            lines.append(rhr_line)
   149→        else:
   150→            lines.append("  • 本周无恢复数据")
   151→        lines.append("")
   152→
   153→        # 5. Body Battery
   154→        lines.append("⚡ *Body Battery*")
   155→        if battery_stats.get("count", 0) > 0:
   156→            lines.append(f"  • 平均充电值：{battery_stats['avg_charged']:.0f}")
   157→            lines.append(f"  • 平均消耗值：{battery_stats['avg_drained']:.0f}")
   158→        else:
   159→            lines.append("  • 本周无 Body Battery 数据")
   160→        lines.append("")
   161→
   162→        # 6. Lifestyle
   163→        lines.append("🥗 *生活方式*")
   164→        if lifestyle_stats.get("has_data"):
   165→            lines.append(f"  • 饮酒天数：{lifestyle_stats['alcohol_days']} 天")
   166→            if lifestyle_stats.get("fasting_modes"):
   167→                modes = ", ".join(
   168→                    f"{m}（{c}天）" for m, c in lifestyle_stats["fasting_modes"].items()
   169→                )
   170→                lines.append(f"  • 禁食模式：{modes}")
   171→            lines.append(f"  • 补剂记录天数：{lifestyle_stats['supplement_days']} 天")
   172→        else:
   173→            lines.append("  • 本周无手动记录")
   174→        lines.append("")
   175→
   176→        # 7. Highlight
   177→        lines.append(f"💡 *本周亮点：* {highlight}")
   178→
   179→        return "\n".join(lines)
   180→
   181→    # ------------------------------------------------------------------
   182→    # Private helpers – data fetching
   183→    # ------------------------------------------------------------------
   184→
   185→    def _get_sleep_stats(self, start: date, end: date) -> dict[str, Any]:
   186→        """Compute sleep statistics for the given date range."""
   187→        records = self.query.get_metric_range("sleep", start, end)
   188→        if not records:
   189→            return {"count": 0}
   190→
   191→        scores = [r["sleep_score"] for r in records if r.get("sleep_score")]
   192→        durations = [r["total_sleep_seconds"] for r in records if r.get("total_sleep_seconds")]
   193→
   194→        if not scores:
   195→            return {"count": 0}
   196→
   197→        best = max(records, key=lambda r: r.get("sleep_score", 0))
   198→        worst = min(records, key=lambda r: r.get("sleep_score", 100))
   199→
   200→        return {
   201→            "count": len(scores),
   202→            "avg_score": sum(scores) / len(scores),
   203→            "avg_duration_sec": int(sum(durations) / len(durations)) if durations else 0,
   204→            "best_date": best.get("date"),
   205→            "best_score": best.get("sleep_score", 0),
   206→            "worst_date": worst.get("date"),
   207→            "worst_score": worst.get("sleep_score", 0),
   208→        }
   209→
   210→    def _get_steps_stats(self, start: date, end: date) -> dict[str, Any]:
   211→        """Compute step statistics for the given date range."""
   212→        records = self.query.get_metric_range("steps", start, end)
   213→        if not records:
   214→            return {"count": 0}
   215→
   216→        step_values = [r["total_steps"] for r in records if r.get("total_steps") is not None]
   217→        if not step_values:
   218→            return {"count": 0}
   219→
   220→        return {
   221→            "count": len(step_values),
   222→            "avg_steps": sum(step_values) / len(step_values),
   223→            "goal_days": sum(1 for s in step_values if s >= 10000),
   224→        }
   225→
   226→    def _get_activity_stats(self, start: date, end: date) -> dict[str, Any]:
   227→        """Compute activity detail statistics from get_activities_range().
   228→
   229→        Args:
   230→            start: Start date (inclusive).
   231→            end: End date (inclusive).
   232→
   233→        Returns:
   234→            Dict with count, activities list, and cumulative HR zone seconds.
   235→        """
   236→        records = self.query.get_activities_range(start, end)
   237→        if not records:
   238→            return {"count": 0, "activities": [], "zone_seconds": [0, 0, 0, 0, 0]}
   239→
   240→        activities: list[dict[str, Any]] = []
   241→        zone_seconds = [0, 0, 0, 0, 0]
   242→
   243→        for r in records:
   244→            raw = r.get("raw_data") or {}
   245→            if isinstance(raw, str):
   246→                import json
   247→                try:
   248→                    raw = json.loads(raw)
   249→                except Exception:
   250→                    raw = {}
   251→
   252→            # Accumulate HR zone seconds (Z1~Z5)
   253→            for i in range(1, 6):
   254→                val = raw.get(f"hrTimeInZone_{i}")
   255→                if val is not None:
   256→                    try:
   257→                        zone_seconds[i - 1] += int(val)
   258→                    except (TypeError, ValueError):
   259→                        pass
   260→
   261→            duration_sec = r.get("duration_seconds") or 0
   262→            activities.append({
   263→                "date": r.get("date", "")[-5:].replace("-", "-") if r.get("date") else "",
   264→                "name": r.get("activity_name") or "未知",
   265→                "duration_min": round(duration_sec / 60) if duration_sec else 0,
   266→                "calories": r.get("calories") or 0,
   267→                "avg_hr": r.get("average_heart_rate") or 0,
   268→                "max_hr": r.get("max_heart_rate") or 0,
   269→            })
   270→
   271→        return {
   272→            "count": len(activities),
   273→            "activities": activities,
   274→            "zone_seconds": zone_seconds,
   275→        }
   276→
   277→    def _get_intensity_stats(self, start: date, end: date) -> dict[str, Any]:
   278→        """Compute intensity minutes vs WHO 150-min target.
   279→
   280→        Args:
   281→            start: Start date (inclusive).
   282→            end: End date (inclusive).
   283→
   284→        Returns:
   285→            Dict with moderate_min, vigorous_min, total_min (WHO-weighted), goal_pct.
   286→        """
   287→        records = self.query.get_metric_range("intensity_minutes", start, end)
   288→        if not records:
   289→            return {"has_data": False}
   290→
   291→        moderate_min = sum(r.get("moderate_minutes") or 0 for r in records)
   292→        vigorous_min = sum(r.get("vigorous_minutes") or 0 for r in records)
   293→        total_min = moderate_min + vigorous_min * 2
   294→        goal_pct = round(total_min / 150 * 100)
   295→
   296→        return {
   297→            "has_data": True,
   298→            "moderate_min": moderate_min,
   299→            "vigorous_min": vigorous_min,
   300→            "total_min": total_min,
   301→            "goal_pct": goal_pct,
   302→        }
   303→
   304→    def _get_recovery_stats(self, start: date, end: date) -> dict[str, Any]:
   305→        """Compute HRV and resting heart rate statistics for the given date range."""
   306→        hrv_records = self.query.get_metric_range("hrv", start, end)
   307→        hr_records = self.query.get_metric_range("heart_rate", start, end)
   308→
   309→        hrv_values = [
   310→            r["hrv_value"]
   311→            for r in hrv_records
   312→            if r.get("hrv_value") is not None
   313→        ]
   314→        rhr_values = [
   315→            r["resting_heart_rate"]
   316→            for r in hr_records
   317→            if r.get("resting_heart_rate")
   318→        ]
   319→
   320→        if not hrv_values and not rhr_values:
   321→            return {"count": 0}
   322→
   323→        return {
   324→            "count": max(len(hrv_values), len(rhr_values)),
   325→            "avg_hrv": sum(hrv_values) / len(hrv_values) if hrv_values else 0.0,
   326→            "avg_rhr": sum(rhr_values) / len(rhr_values) if rhr_values else 0.0,
   327→        }
   328→
   329→    def _get_battery_stats(self, start: date, end: date) -> dict[str, Any]:
   330→        """Compute Body Battery charge/drain statistics for the given date range."""
   331→        records = self.query.get_metric_range("body_battery", start, end)
   332→        if not records:
   333→            return {"count": 0}
   334→
   335→        charged = [r["charged"] for r in records if r.get("charged") is not None]
   336→        drained = [r["drained"] for r in records if r.get("drained") is not None]
   337→
   338→        if not charged and not drained:
   339→            return {"count": 0}
   340→
   341→        return {
   342→            "count": len(records),
   343→            "avg_charged": sum(charged) / len(charged) if charged else 0.0,
   344→            "avg_drained": sum(drained) / len(drained) if drained else 0.0,
   345→        }
   346→
   347→    def _get_lifestyle_stats(self, start: date, end: date) -> dict[str, Any]:
   348→        """Compute lifestyle log statistics (alcohol, fasting, supplements)."""
   349→        logs = self.manual_storage.get_logs_in_range(start, end)
   350→        if not logs:
   351→            return {"has_data": False}
   352→
   353→        alcohol_days = sum(1 for log in logs if log.alcohol_entries)
   354→        supplement_days = sum(1 for log in logs if log.supplement_entries)
   355→
   356→        fasting_modes: dict[str, int] = {}
   357→        for log in logs:
   358→            if log.fasting_mode:
   359→                fasting_modes[log.fasting_mode] = fasting_modes.get(log.fasting_mode, 0) + 1
   360→
   361→        return {
   362→            "has_data": True,
   363→            "alcohol_days": alcohol_days,
   364→            "supplement_days": supplement_days,
   365→            "fasting_modes": fasting_modes,
   366→        }
   367→
   368→    # ------------------------------------------------------------------
   369→    # Private helpers – formatting
   370→    # ------------------------------------------------------------------
   371→
   372→    def _format_duration(self, seconds: int) -> str:
   373→        """Format a duration in seconds to 'Xh Ym' string.
   374→
   375→        Args:
   376→            seconds: Total duration in seconds.
   377→
   378→        Returns:
   379→            Human-readable duration string, e.g. '7h 23m'.
   380→        """
   381→        if seconds <= 0:
   382→            return "0h 0m"
   383→        hours = seconds // 3600
   384→        minutes = (seconds % 3600) // 60
   385→        return f"{hours}h {minutes}m"
   386→
   387→    def _pick_highlight(self, stats: dict[str, Any]) -> str:
   388→        """Select the most noteworthy insight from aggregated stats.
   389→
   390→        Args:
   391→            stats: Dictionary containing all section stats.
   392→
   393→        Returns:
   394→            A single highlight sentence in Chinese.
   395→        """
   396→        highlights: list[tuple[float, str]] = []
   397→
   398→        sleep = stats.get("sleep", {})
   399→        if sleep.get("count", 0) > 0:
   400→            score = sleep["avg_score"]
   401→            if score >= 85:
   402→                highlights.append((score, f"睡眠质量优秀，平均得分 {score:.0f} 分 🌟"))
   403→            elif score < 65:
   404→                highlights.append((100 - score, f"睡眠质量偏低，平均得分 {score:.0f} 分，建议关注"))
   405→
   406→        steps = stats.get("steps", {})
   407→        if steps.get("count", 0) > 0:
   408→            goal_days = steps["goal_days"]
   409→            if goal_days >= 5:
   410→                highlights.append((goal_days * 10, f"本周 {goal_days} 天达到万步目标，运动坚持度佳 💪"))
   411→            elif goal_days == 0:
   412→                highlights.append((60.0, "本周没有达到万步目标，需要增加日常活动量"))
   413→
   414→        intensity = stats.get("intensity", {})
   415→        if intensity.get("has_data"):
   416→            pct = intensity["goal_pct"]
   417→            total_min = intensity["total_min"]
   418→            if pct >= 100:
   419→                highlights.append((90.0, f"本周强度分钟达标（{total_min} min），运动量充足 🎯"))
   420→            elif pct < 50:
   421→                highlights.append((70.0, f"强度分钟仅 {total_min} min，不足 WHO 目标的一半，建议增加有氧运动"))
   422→
   423→        recovery = stats.get("recovery", {})
   424→        if recovery.get("count", 0) > 0 and recovery.get("avg_hrv", 0) > 0:
   425→            hrv = recovery["avg_hrv"]
   426→            if hrv >= 60:
   427→                highlights.append((hrv, f"HRV 均值 {hrv:.0f} ms，恢复状态良好 ✅"))
   428→            elif hrv < 35:
   429→                highlights.append((80.0, f"HRV 偏低（{hrv:.0f} ms），注意休息与压力管理"))
   430→
   431→        lifestyle = stats.get("lifestyle", {})
   432→        if lifestyle.get("has_data") and lifestyle.get("alcohol_days", 0) == 0:
   433→            highlights.append((50.0, "本周零饮酒，生活方式健康 🌿"))
   434→
   435→        if not highlights:
   436→            return "数据积累中，下周见更多洞察。"
   437→
   438→        highlights.sort(key=lambda x: x[0], reverse=True)
   439→        return highlights[0][1]
   440→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→from health.analytics.engine import HealthAnalyst
     2→from health.utils.logging_config import setup_logger
     3→
     4→logger = setup_logger(__name__)
     5→
     6→class AnalyticsTool:
     7→    @staticmethod
     8→    def get_health_insights(analysis_type: str, lookback_days: int = 30) -> str:
     9→        """
    10→        Get advanced health insights and correlations.
    11→
    12→        Args:
    13→            analysis_type: Type of analysis to run. Options:
    14→                - 'recovery_correlations': Impact of Alcohol/Activity on Sleep/RHR.
    15→                - 'fitness_trends': Trends in RHR vs Activity over time.
    16→                - 'lifestyle_impact': Efficacy of Fasting and Supplements.
    17→            lookback_days: Number of days to analyze (default 30, max 365).
    18→        """
    19→        try:
    20→            analyst = HealthAnalyst()
    21→
    22→            if analysis_type == "recovery_correlations":
    23→                results = analyst.analyze_recovery_correlations(days=lookback_days)
    24→                return f"Recovery Correlations (Last {lookback_days} days):\n{results}"
    25→
    26→            elif analysis_type == "fitness_trends":
    27→                results = analyst.analyze_fitness_trends(days=lookback_days)
    28→                return f"Fitness Trends (Last {lookback_days} days):\n{results}"
    29→
    30→            elif analysis_type == "lifestyle_impact":
    31→                results = analyst.analyze_lifestyle_impact(days=lookback_days)
    32→                return f"Lifestyle Impact Analysis (Last {lookback_days} days):\n{results}"
    33→
    34→            else:
    35→                return f"Unknown analysis type: {analysis_type}. Supported: recovery_correlations, fitness_trends, lifestyle_impact."
    36→
    37→        except Exception as e:
    38→            logger.error(f"Error generating insights: {e}")
    39→            return f"Error analyzing data: {str(e)}"
    40→
    41→    @staticmethod
    42→    def analyze_driver(driver_metric: str, target_metric: str, method: str = "correlation", lookback_days: int = 30) -> str:
    43→        """
    44→        Analyze specific relationships between two metrics.
    45→
    46→        Args:
    47→            driver_metric: The cause (e.g., 'alcohol_units', 'steps', 'fasting_mode', 'has_magnesium').
    48→            target_metric: The effect (e.g., 'sleep', 'rhr', 'hrv').
    49→            method: 'correlation' (for continuous values) or 'group_compare' (for yes/no events).
    50→            lookback_days: Days to analyze.
    51→        """
    52→        try:
    53→            analyst = HealthAnalyst()
    54→
    55→            if method == "correlation":
    56→                # Default to 0 lag for general, but maybe 1 for recovery?
    57→                # Let's default to same-day unless it's known delayed metrics
    58→                lag = 0
    59→                if target_metric in ['sleep', 'rhr', 'hrv'] and driver_metric in ['alcohol_units', 'stress']:
    60→                    lag = 1 # Assumptions for common drivers
    61→
    62→                result = analyst.analyze_lagged_correlation(driver_metric, target_metric, lag=lag, days=lookback_days)
    63→                return f"Correlation Analysis ({driver_metric} -> {target_metric}, lag={lag}d):\n{result}"
    64→
    65→            elif method == "group_compare":
    66→                result = analyst.compare_groups(driver_metric, target_metric, days=lookback_days)
    67→                return f"Group Comparison ({driver_metric} YES vs NO -> {target_metric}):\n{result}"
    68→
    69→            else:
    70→                return f"Unknown method: {method}"
    71→
    72→        except Exception as e:
    73→            logger.error(f"Error in analyze_driver: {e}")
    74→            return f"Error: {e}"
    75→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now let me check the config to understand available data types and explore if there are any other analysis files:


/root/projects/butler/health/utils/__init__.py
/root/projects/butler/health/utils/date_utils.py
/root/projects/butler/health/utils/exceptions.py
/root/projects/butler/health/__init__.py
/root/projects/butler/health/db/__init__.py
/root/projects/butler/health/db/schema.py
/root/projects/butler/health/db/repository.py
/root/projects/butler/health/models/__init__.py
/root/projects/butler/health/models/sync_metadata.py
/root/projects/butler/health/services/__init__.py
/root/projects/butler/health/cli/__init__.py
/root/projects/butler/health/cli/sync.py
/root/projects/butler/health/models/body_metrics.py
/root/projects/butler/health/models/activity.py
/root/projects/butler/health/services/query.py
/root/projects/butler/health/cli/query.py
/root/projects/butler/health/services/data_sync.py
/root/projects/butler/health/models/manual_log.py
/root/projects/butler/health/services/manual_log_storage.py
/root/projects/butler/health/cli/manual.py
/root/projects/butler/health/services/obsidian.py
/root/projects/butler/health/cli/obsidian.py
/root/projects/butler/health/utils/logging_config.py
/root/projects/butler/health/analytics/__init__.py
/root/projects/butler/health/analytics/engine.py
/root/projects/butler/health/utils/env_loader.py
/root/projects/butler/health/analytics/weekly_report.py
/root/projects/butler/health/utils/time_utils.py
/root/projects/butler/health/models/daily_metrics.py
/root/projects/butler/health/config.py
/root/projects/butler/health/services/storage.py
/root/projects/butler/health/services/garmin_client.py