/root/projects/butler
├── BOT_FEATURES_REFERENCE.md
├── BOT_FIX_SUMMARY.md
├── bulter-tag20260215.tgz
├── Butler-Bot使用指南.md
├── butler-tag20260302.tgz
├── CLAUDE.md
├── CONFIGURATION_SUMMARY.md
├── data
│   ├── health
│   │   ├── activities
│   │   ├── body_metrics
│   │   ├── bot_pids.json
│   │   ├── daily_metrics
│   │   ├── health.db
│   │   ├── health.db.bak_20260302
│   │   ├── manual_logs
│   │   ├── obsidian_context
│   │   └── slack_context
│   └── proxy_health_history.jsonl
├── debug_failed_payload.json
├── debug_request.json
├── debug_response.json
├── decision.md
├── distilled_insights.md
├── docs
│   ├── context_management.md
│   ├── CRON_SETUP.md
│   ├── enhanced_logging_guide.md
│   ├── gemini_empty_response_diagnosis.md
│   ├── gemini_root_cause_analysis.md
│   ├── gmode_check_detailed.md
│   ├── health_query_usage.md
│   ├── HEALTH_SYNC_README.md
│   ├── log_analysis_2026-02-06.md
│   ├── MANUAL_ENTRY.md
│   ├── proxy_decision_guide.md
│   ├── proxy_issue_final_analysis.md
│   ├── QUICK_START_AFTER_RETURN.md
│   ├── REVIEW_AND_ROADMAP.md
│   ├── safety_override_restored.md
│   ├── steps_fix_summary.md
│   ├── TESTING.md
│   └── TODO.md
├── experiments
│   ├── CLEANUP_SUMMARY.md
│   ├── compare_dispatchers.py
│   ├── DIAGNOSIS_REPORT.md
│   ├── dispatcher_clean.py
│   ├── fix_model.sh
│   └── test_llm_intelligence.py
├── health
│   ├── analytics
│   │   ├── engine.py
│   │   ├── __init__.py
│   │   └── weekly_report.py
│   ├── cli
│   │   ├── __init__.py
│   │   ├── manual.py
│   │   ├── obsidian.py
│   │   ├── query.py
│   │   └── sync.py
│   ├── config.py
│   ├── db
│   │   ├── __init__.py
│   │   ├── repository.py
│   │   └── schema.py
│   ├── __init__.py
│   ├── models
│   │   ├── activity.py
│   │   ├── body_metrics.py
│   │   ├── daily_metrics.py
│   │   ├── __init__.py
│   │   ├── manual_log.py
│   │   └── sync_metadata.py
│   ├── services
│   │   ├── data_sync.py
│   │   ├── garmin_client.py
│   │   ├── __init__.py
│   │   ├── manual_log_storage.py
│   │   ├── obsidian.py
│   │   ├── query.py
│   │   └── storage.py
│   └── utils
│       ├── date_utils.py
│       ├── env_loader.py
│       ├── exceptions.py
│       ├── __init__.py
│       ├── logging_config.py
│       └── time_utils.py
├── health.md
├── h.sh
├── list_samples.py
├── logs
│   ├── daily_sync.log
│   ├── health.log
│   ├── health_sync.log
│   ├── obsidian.log
│   ├── shell.log
│   ├── slack_bot.log
│   └── weekly_report.log
├── m1.py
├── m2.py
├── mannul-data-input.md
├── methodology.md
├── monitor-current-claude-endpoint.sh
├── POWELL_REVISE.md
├── quick_test.py
├── README.md
├── REPLY-SAMPLE.md
├── requirements.txt
├── scripts
│   ├── analyze_cost.py
│   ├── analyze_recent_health.py
│   ├── bot_manager.py
│   ├── compare_local_vs_openrouter.py
│   ├── compare_with_without_tools.py
│   ├── daily_sync.py
│   ├── debug_empty_response.py
│   ├── deep_analyze_local_proxy.py
│   ├── diagnose_gemini.py
│   ├── diagnose_shell_bot.py
│   ├── force_sync_today.py
│   ├── health_check.py
│   ├── migrate_commands.py
│   ├── monitor_proxy_health.py
│   ├── obsidian_bot.py
│   ├── obsidian_distiller.py
│   ├── obsidian_tool.py
│   ├── query_examples.py
│   ├── resync_steps.py
│   ├── run_tests.py
│   ├── select_gemini.py
│   ├── setup_health.py
│   ├── simulate_full_bot_flow.py
│   ├── switch_proxy.sh
│   ├── test_binary_search_prompt.py
│   ├── test_clean_minimal.py
│   ├── test_direct_google_api.py
│   ├── test_exact_scenario.py
│   ├── test_length_limit.py
│   ├── test_light_mode.py
│   ├── test_one_simple_tool.py
│   ├── test_progressive_prompt.py
│   ├── test_progressive_tools.py
│   ├── test_proxy_workaround.py
│   ├── test_safety_override.py
│   ├── test_smart_quotes.py
│   ├── test_stability.py
│   ├── test_system_prompt_impact.py
│   ├── test_tool_choice.py
│   ├── test_tool_count_impact.py
│   ├── tm.py
│   ├── verify_imports.py
│   ├── verify_obsidian_tools.py
│   ├── verify_sync.py
│   ├── verify_system.py
│   ├── verify_time.py
│   └── weekly_report.py
├── skills
│   ├── health_sync.py
│   ├── log_diet_simple.py
│   ├── log_fasting_simple.py
│   ├── log_supplement_simple.py
│   └── obsidian_sync.py
├── slack_bot
│   ├── context
│   │   ├── compressor.py
│   │   └── storage.py
│   ├── dispatcher.backup.py
│   ├── dispatcher.py
│   ├── __init__.py
│   ├── llm
│   │   ├── base.py
│   │   └── gemini.py
│   ├── main.py
│   ├── obsidian
│   │   ├── cli.py
│   │   ├── dispatcher.py
│   │   ├── generators.py
│   │   ├── indexer.py
│   │   └── __init__.py
│   ├── shell
│   │   ├── help_text.py
│   │   ├── __init__.py
│   │   ├── manager.py
│   │   └── session.py
│   ├── shell_main.py
│   ├── tools
│   │   ├── analytics.py
│   │   ├── file_ops.py
│   │   ├── garmin.py
│   │   ├── groups.py
│   │   ├── health_read.py
│   │   ├── health_write.py
│   │   ├── obsidian.py
│   │   ├── process_ops.py
│   │   ├── registry.py
│   │   ├── shell_tool.py
│   │   ├── system_ops.py


/root/projects/butler/tests/health/test_analytics.py
/root/projects/butler/tests/integration/__init__.py
/root/projects/butler/tests/integration/test_cli_scenarios.py
/root/projects/butler/tests/integration/test_live_brain.py
/root/projects/butler/tests/slack_bot/scenarios/__init__.py
/root/projects/butler/tests/slack_bot/scenarios/test_e2e.py
/root/projects/butler/tests/slack_bot/unit/__init__.py
/root/projects/butler/tests/slack_bot/unit/test_context.py
/root/projects/butler/tests/slack_bot/unit/test_dispatcher.py
/root/projects/butler/tests/slack_bot/unit/test_gemini_json.py
/root/projects/butler/tests/slack_bot/unit/test_tools_registry.py
/root/projects/butler/tests/slack_bot/__init__.py
/root/projects/butler/tests/slack_bot/test_shell_tools.py
/root/projects/butler/tests/unit/test_gemini_resilience.py
/root/projects/butler/tests/unit/test_tools_schema.py
/root/projects/butler/tests/__init__.py
/root/projects/butler/tests/conftest.py
/root/projects/butler/tests/test_context_commands.py
/root/projects/butler/tests/test_garmin_client_new_features.py
/root/projects/butler/tests/test_mrkdwn.py


Perfect! Let me now read a couple more critical files to complete the picture:


     1→# 🤖 Butler - 智能个人助理系统
     2→
     3→> 一个企业级质量的模块化个人助理系统，专注于数据驱动的健康追踪、生物黑客实验和桌面自动化
     4→
     5→[![Python](https://img.shields.io/badge/Python-3.12-blue.svg)](https://www.python.org/)
     6→[![Code Quality](https://img.shields.io/badge/Code%20Quality-Enterprise-green.svg)](docs/REVIEW_AND_ROADMAP.md)
     7→[![Architecture](https://img.shields.io/badge/Architecture-⭐⭐⭐⭐⭐-yellow.svg)](docs/ARCHITECTURE.md)
     8→[![License](https://img.shields.io/badge/License-Private-red.svg)](LICENSE)
     9→
    10→---
    11→
    12→## 📋 目录
    13→
    14→- [项目概览](#-项目概览)
    15→- [核心特性](#-核心特性)
    16→- [设计原则](#-设计原则)
    17→- [架构设计](#-架构设计)
    18→- [技术栈](#-技术栈)
    19→- [快速开始](#-快速开始)
    20→- [模块详解](#-模块详解)
    21→- [使用指南](#-使用指南)
    22→- [开发规范](#-开发规范)
    23→- [性能指标](#-性能指标)
    24→- [未来规划](#-未来规划)
    25→- [贡献指南](#-贡献指南)
    26→
    27→---
    28→
    29→## 🎯 项目概览
    30→
    31→Butler 是一个为生物黑客和数据驱动决策者设计的**个人 AI 助理系统**。它将健康数据管理、智能分析和桌面自动化无缝集成，通过 Slack Bot 提供自然语言交互界面，让你可以像与私人助理对话一样管理个人数据。
    32→
    33→### 为什么选择 Butler?
    34→
    35→- 🏥 **全面的健康数据管理**: 自动同步 Garmin 设备数据，涵盖 14+ 种健康指标
    36→- 🤖 **智能 AI 助理**: 基于 Gemini 2.0 的 Slack Bot，支持自然语言查询和复杂工具调用
    37→- 📊 **高级数据分析**: Pandas 驱动的相关性分析、趋势预测和驱动因素识别
    38→- 🔒 **隐私优先**: 所有数据本地存储，无云依赖，完全掌控你的数据
    39→- 🛠 **高度可扩展**: 模块化架构，轻松添加新功能和数据源
    40→- 💻 **桌面自动化**: 持久化 Shell 会话，执行系统任务和脚本
    41→
    42→### 项目统计
    43→
    44→```
    45→核心代码:      ~10,600 行 Python
    46→模块数量:      3 个主要模块 (Health, Obsidian, Shell)
    47→工具数量:      14 个 AI 可调用工具
    48→健康指标:      14+ 种 (睡眠、心率、HRV、压力等)
    49→数据存储:      104 MB (SQLite + JSON)
    50→代码质量:      ⭐⭐⭐⭐⭐ (5/5)
    51→总体评分:      ⭐⭐⭐⭐☆ (4.1/5)
    52→```
    53→
    54→---
    55→
    56→## ✨ 核心特性
    57→
    58→### 1. 🏥 健康数据管理
    59→
    60→#### Garmin 自动同步
    61→- **增量同步**: 智能检测最后同步日期，只同步新数据
    62→- **批处理**: 历史数据回填，避免 API 限流
    63→- **14+ 健康指标**:
    64→  - 睡眠 (总睡眠、深睡、REM、睡眠评分)
    65→  - 心率 (静息心率、最大/最小/平均心率、心率区间)
    66→  - HRV (心率变异性、基线、状态)
    67→  - 压力 (平均/最大压力、时间分布)
    68→  - Body Battery (充电/消耗、最高/最低值)
    69→  - 步数、距离、卡路里、楼层
    70→  - 血氧饱和度 (SpO2)
    71→  - 呼吸率、水合作用
    72→  - 运动活动 (类型、时长、距离、心率、配速)
    73→
    74→#### 手动记录系统
    75→- **饮食日志**: 餐食类型、时间、描述、卡路里
    76→- **补剂追踪**: 名称、剂量、时间
    77→- **酒精记录**: 类型、数量、时间
    78→- **感受日志**: 症状类型、严重程度、描述
    79→- **禁食模式**: OMAD、PSMF 等饮食方案追踪
    80→
    81→#### 高级分析引擎
    82→```python
    83→# 相关性分析: 酒精对睡眠的影响
    84→analyze_driver(
    85→    driver_metric="alcohol_units",
    86→    target_metric="sleep_score",
    87→    method="correlation"
    88→)
    89→
    90→# 趋势分析: HRV 和静息心率趋势
    91→get_health_insights(
    92→    analysis_type="fitness_trends",
    93→    lookback_days=90
    94→)
    95→
    96→# 生活方式影响: 禁食对 Body Battery 的影响
    97→get_health_insights(
    98→    analysis_type="lifestyle_impact",
    99→    lookback_days=60
   100→)
   101→```
   102→
   103→---
   104→
   105→### 2. 🤖 Slack AI Bot
   106→
   107→#### 双 Bot 架构
   108→- **Health Bot**: 专注健康数据查询、记录和分析
   109→- **Shell Bot**: 系统管理和命令执行
   110→
   111→#### 14 个 AI 工具
   112→
   113→| 类别 | 工具名称 | 功能 |
   114→|------|---------|------|
   115→| **读取** | `get_daily_detailed_stats` | 获取某日完整健康摘要 |
   116→|  | `get_metric_history` | 查询指标历史趋势 |
   117→|  | `get_activity_history` | 获取运动记录 |
   118→|  | `get_manual_history` | 查询手动日志 |
   119→| **写入** | `log_diet` | 记录饮食 |
   120→|  | `log_alcohol` | 记录酒精 |
   121→|  | `log_supplement` | 记录补剂 |
   122→|  | `log_feeling` | 记录身体感受 |
   123→|  | `log_fasting` | 记录禁食模式 |
   124→| **同步** | `sync_garmin` | 同步 Garmin 数据 |
   125→|  | `sync_obsidian` | 同步 Obsidian 笔记 |
   126→| **分析** | `get_health_insights` | 高级健康洞察 |
   127→|  | `analyze_driver` | 驱动因素分析 |
   128→| **系统** | `search_web` | 网络搜索 |
   129→|  | `execute_shell` | 执行 Shell 命令 |
   130→
   131→#### 智能对话示例
   132→
   133→```
   134→你: 昨天我睡得怎么样?
   135→Bot: [调用 get_daily_detailed_stats(2024-01-22)]
   136→     昨天你的睡眠质量不错:
   137→     - 总睡眠: 7小时32分
   138→     - 睡眠评分: 82/100
   139→     - 深睡: 1小时45分
   140→     - REM: 1小时58分
   141→     - 早上静息心率: 52 bpm
   142→
   143→你: 过去一周酒精对我睡眠有什么影响?
   144→Bot: [调用 analyze_driver(alcohol, sleep_score)]
   145→     分析过去 7 天的数据:
   146→     - 相关系数: -0.68 (中度负相关)
   147→     - 喝酒后睡眠评分平均下降 12 分
   148→     - 建议: 睡前 4 小时避免饮酒
   149→
   150→你: 帮我记录今天午餐吃了鸡胸肉沙拉

<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 datetime import date, datetime
     2→from typing import Optional
     3→from health.services.manual_log_storage import ManualLogStorage
     4→from health.models.manual_log import (
     5→    DietEntry,
     6→    AlcoholEntry,
     7→    SupplementEntry,
     8→    BodyFeelingEntry
     9→)
    10→from health.utils.logging_config import setup_logger
    11→
    12→logger = setup_logger(__name__)
    13→
    14→class HealthWriter:
    15→    @staticmethod
    16→    def log_diet(
    17→        target_date: str,
    18→        description: str,
    19→        time: Optional[str] = None,
    20→        meal_type: str = "snack"
    21→    ) -> str:
    22→        """Log a diet entry."""
    23→        try:
    24→            date_obj = date.fromisoformat(target_date)
    25→            time_str = time or datetime.now().strftime("%H:%M")
    26→
    27→            entry = DietEntry(
    28→                time=time_str,
    29→                description=description,
    30→                meal_type=meal_type
    31→            )
    32→
    33→            storage = ManualLogStorage()
    34→            storage.add_diet_entry(date_obj, entry)
    35→            return f"✅ Logged diet for {target_date}: {description}"
    36→        except Exception as e:
    37→            logger.error(f"Failed to log diet: {e}")
    38→            return f"❌ Error logging diet: {str(e)}"
    39→
    40→    @staticmethod
    41→    def log_alcohol(
    42→        target_date: str,
    43→        alcohol_type: str,
    44→        amount: str,
    45→        time: Optional[str] = None
    46→    ) -> str:
    47→        """Log alcohol consumption."""
    48→        try:
    49→            date_obj = date.fromisoformat(target_date)
    50→            time_str = time or datetime.now().strftime("%H:%M")
    51→
    52→            entry = AlcoholEntry(
    53→                time=time_str,
    54→                alcohol_type=alcohol_type,
    55→                amount=amount
    56→            )
    57→
    58→            storage = ManualLogStorage()
    59→            storage.add_alcohol_entry(date_obj, entry)
    60→            return f"✅ Logged alcohol for {target_date}: {amount} {alcohol_type}"
    61→        except Exception as e:
    62→            logger.error(f"Failed to log alcohol: {e}")
    63→            return f"❌ Error logging alcohol: {str(e)}"
    64→
    65→    @staticmethod
    66→    def log_supplement(
    67→        target_date: str,
    68→        name: str,
    69→        dosage: str,
    70→        time: Optional[str] = None
    71→    ) -> str:
    72→        """Log supplement."""
    73→        try:
    74→            date_obj = date.fromisoformat(target_date)
    75→            time_str = time or datetime.now().strftime("%H:%M")
    76→
    77→            entry = SupplementEntry(
    78→                time=time_str,
    79→                supplement_name=name,
    80→                dosage=dosage

<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:

<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>


Excellent! Now let me create a comprehensive summary document. Let me gather the complete directory listing to ensure I have everything:


Bash

find /root/projects/butler -type f \( -name "*.py" -o -name "*.md" \) | grep -v -E "(venv/|__pycache__|tag202602|\.git|\.pyc)" | wc -l


find /root/projects/butler -type d -name "tests" -o -type d -name "health" | head -5


total 32
drwxr-xr-x 3  501 games    57 Mar  2 09:48 .
drwxr-xr-x 9  501 games  4096 Mar  2 10:38 ..
-rw-r--r-- 1  501 games   856 Jan 20 17:25 base.py
-rw-r--r-- 1  501 games 22677 Feb  6 11:43 gemini.py
drwxr-xr-x 2 root root     64 Mar  2 09:48 __pycache__


/root/projects/butler/data/health
/root/projects/butler/health
/root/projects/butler/tag20260215/health
/root/projects/butler/tag20260215/tests
/root/projects/butler/tag20260215/tests/health


Perfect! Now I have all the information I need. Let me compile a comprehensive architecture documentation report:


find /root/projects/butler -type f -name "*.py" | grep -E "(slack_bot|health)" | grep -v -E "(venv|__pycache__|tag|test)" | sort


/root/projects/butler/health/analytics/engine.py
/root/projects/butler/health/analytics/__init__.py
/root/projects/butler/health/analytics/weekly_report.py
/root/projects/butler/health/cli/__init__.py
/root/projects/butler/health/cli/manual.py
/root/projects/butler/health/cli/obsidian.py
/root/projects/butler/health/cli/query.py
/root/projects/butler/health/cli/sync.py
/root/projects/butler/health/config.py
/root/projects/butler/health/db/__init__.py
/root/projects/butler/health/db/repository.py
/root/projects/butler/health/db/schema.py
/root/projects/butler/health/__init__.py
/root/projects/butler/health/models/activity.py
/root/projects/butler/health/models/body_metrics.py
/root/projects/butler/health/models/daily_metrics.py
/root/projects/butler/health/models/__init__.py
/root/projects/butler/health/models/manual_log.py
/root/projects/butler/health/models/sync_metadata.py
/root/projects/butler/health/services/data_sync.py
/root/projects/butler/health/services/garmin_client.py
/root/projects/butler/health/services/__init__.py
/root/projects/butler/health/services/manual_log_storage.py
/root/projects/butler/health/services/obsidian.py
/root/projects/butler/health/services/query.py
/root/projects/butler/health/services/storage.py
/root/projects/butler/health/utils/date_utils.py
/root/projects/butler/health/utils/env_loader.py
/root/projects/butler/health/utils/exceptions.py
/root/projects/butler/health/utils/__init__.py
/root/projects/butler/health/utils/logging_config.py
/root/projects/butler/health/utils/time_utils.py
/root/projects/butler/scripts/analyze_recent_health.py
/root/projects/butler/scripts/health_check.py
/root/projects/butler/scripts/monitor_proxy_health.py
/root/projects/butler/scripts/setup_health.py
/root/projects/butler/skills/health_sync.py
/root/projects/butler/slack_bot/context/compressor.py
/root/projects/butler/slack_bot/context/storage.py
/root/projects/butler/slack_bot/dispatcher.backup.py
/root/projects/butler/slack_bot/dispatcher.py
/root/projects/butler/slack_bot/__init__.py
/root/projects/butler/slack_bot/llm/base.py
/root/projects/butler/slack_bot/llm/gemini.py
/root/projects/butler/slack_bot/main.py
/root/projects/butler/slack_bot/obsidian/cli.py
/root/projects/butler/slack_bot/obsidian/dispatcher.py
/root/projects/butler/slack_bot/obsidian/generators.py
/root/projects/butler/slack_bot/obsidian/indexer.py
/root/projects/butler/slack_bot/obsidian/__init__.py
/root/projects/butler/slack_bot/shell/help_text.py
/root/projects/butler/slack_bot/shell/__init__.py
/root/projects/butler/slack_bot/shell_main.py
/root/projects/butler/slack_bot/shell/manager.py
/root/projects/butler/slack_bot/shell/session.py
/root/projects/butler/slack_bot/tools/analytics.py
/root/projects/butler/slack_bot/tools/file_ops.py
/root/projects/butler/slack_bot/tools/garmin.py
/root/projects/butler/slack_bot/tools/groups.py
/root/projects/butler/slack_bot/tools/health_read.py
/root/projects/butler/slack_bot/tools/health_write.py
/root/projects/butler/slack_bot/tools/obsidian.py
/root/projects/butler/slack_bot/tools/process_ops.py
/root/projects/butler/slack_bot/tools/registry.py
/root/projects/butler/slack_bot/tools/shell_tool.py
/root/projects/butler/slack_bot/tools/system_ops.py
/root/projects/butler/slack_bot/tools/web.py
/root/projects/butler/slack_bot/utils/mrkdwn.py


Perfect! Now I have a complete picture. Let me provide you with a comprehensive architecture documentation report:

---

# BUTLER PROJECT - COMPLETE ARCHITECTURE DOCUMENTATION

## 1. FULL DIRECTORY TREE (Production Code Only)

```
/root/projects/butler/
├── README.md                          # Main project documentation
├── CLAUDE.md                          # Developer instructions & standards
├── CONFIGURATION_SUMMARY.md           # Configuration reference
├── BOT_FEATURES_REFERENCE.md         # Bot capabilities reference
├── health.md                          # User health profile
│
├── health/                            # Health Data Management Module (Core)
│   ├── __init__.py
│   ├── config.py                      # Configuration: paths, DATA_TYPE_CONFIG (14 metric types)
│   ├── analytics/                     # Advanced analytics engine
│   │   ├── engine.py                  # Correlation analysis, driver detection
│   │   ├── weekly_report.py          # Weekly report generation
│   │   └── __init__.py
│   ├── db/                            # Database layer (SQLite WAL)
│   │   ├── schema.py                  # Table definitions: sync_records, daily_metrics_index, activity_index
│   │   ├── repository.py              # CRUD operations for sync records & indexes
│   │   └── __init__.py
│   ├── models/                        # Pydantic data models
│   │   ├── daily_metrics.py          # Sleep, HeartRate, HRV, Stress, BodyBattery, etc.
│   │   ├── activity.py               # Exercise activities (type, duration, distance, HR)
│   │   ├── body_metrics.py           # Weight, BMI
│   │   ├── manual_log.py             # Diet, Alcohol, Supplements, Feelings, Fasting
│   │   ├── sync_metadata.py          # Sync tracking metadata
│   │   └── __init__.py
│   ├── services/                      # Business logic layer
│   │   ├── data_sync.py              # Orchestration: GarminHealthClient + Storage + Repository
│   │   ├── garmin_client.py          # Garmin Connect API wrapper (14 fetch methods, retry logic)
│   │   ├── query.py                  # Query service: get_daily_summary, get_metric_range, activities
│   │   ├── storage.py                # File-based storage: JSON + directory structure
│   │   ├── manual_log_storage.py    # Manual log persistence (diet, alcohol, etc.)
│   │   ├── obsidian.py              # Obsidian vault integration
│   │   └── __init__.py
│   ├── utils/                         # Utilities
│   │   ├── date_utils.py             # Date parsing, ranges, formatting
│   │   ├── time_utils.py             # Time zone handling (UTC+8)
│   │   ├── env_loader.py             # Environment variable loading with extras
│   │   ├── logging_config.py         # Centralized logging setup
│   │   ├── exceptions.py             # Custom exceptions (SyncError, GarminAPIError)
│   │   └── __init__.py
│   └── cli/                           # CLI interfaces
│       ├── sync.py                    # CLI: health sync
│       ├── query.py                   # CLI: health query
│       ├── manual.py                  # CLI: health log-diet, log-supplement
│       ├── obsidian.py               # CLI: health sync-obsidian
│       └── __init__.py
│
├── slack_bot/                         # Slack AI Bot Module
│   ├── __init__.py
│   ├── main.py                        # Bot entry point: SocketModeHandler setup
│   ├── dispatcher.py                  # Core message dispatcher (580+ lines)
│   ├── shell_main.py                  # Shell Bot entry point (disabled by default)
│   ├── context/                       # Conversation context management
│   │   ├── storage.py                 # Channel-level context persistence (40 msg sliding window)
│   │   ├── compressor.py             # Context compression for large histories
│   │   └── __init__.py
│   ├── llm/                           # LLM integration layer
│   │   ├── base.py                    # BaseLLM abstract class
│   │   ├── gemini.py                  # GeminiLLM: API wrapper for Gemini models
│   │   └── __init__.py
│   ├── tools/                         # AI-callable tools (21 tools total)
│   │   ├── registry.py               # TOOLS_SCHEMA + TOOL_FUNCTIONS mapping (382 lines)
│   │   ├── groups.py                 # Tool presets: light(6), standard(10), full(15), all(21)
│   │   ├── health_read.py            # HealthReader: get_daily_detailed_stats, get_metric_history, etc.
│   │   ├── health_write.py           # HealthWriter: log_diet, log_alcohol, log_supplement, etc.
│   │   ├── analytics.py              # AnalyticsTool: get_health_insights, analyze_driver
│   │   ├── garmin.py                 # GarminTool: sync_garmin
│   │   ├── obsidian.py               # ObsidianTool: sync_obsidian
│   │   ├── web.py                    # WebSearchTool: search_web
│   │   ├── shell_tool.py             # execute_shell with persistent session
│   │   ├── file_ops.py               # download_file, save_file
│   │   ├── system_ops.py             # get_system_status (CPU, Memory, Disk)
│   │   ├── process_ops.py            # kill_process, get_process_list
│   │   └── __init__.py
│   ├── shell/                         # Shell Bot components
│   │   ├── manager.py                # Process manager for persistent shell sessions
│   │   ├── session.py                # Persistent shell session with history
│   │   ├── help_text.py              # Shell Bot help and command documentation
│   │   └── __init__.py
│   ├── obsidian/                      # Obsidian vault integration
│   │   ├── dispatcher.py             # Obsidian message dispatcher
│   │   ├── indexer.py                # Obsidian file indexing
│   │   ├── generators.py             # Daily note generation
│   │   ├── cli.py                    # Obsidian CLI interface
│   │   └── __init__.py
│   ├── utils/                         # Bot utilities
│   │   ├── mrkdwn.py                 # Slack MarkDown formatting (text → mrkdwn conversion)
│   │   └── __init__.py
│   └── __init__.py
│
├── scripts/                           # Standalone scripts and utilities
│   ├── bot_manager.py                 # Start/stop/status health and shell bots
│   ├── daily_sync.py                  # Daily cron: sync Garmin data
│   ├── weekly_report.py               # Weekly cron: generate health report
│   ├── health_check.py                # Health system diagnostics
│   ├── setup_health.py                # Initial setup and configuration
│   ├── (20+ other test/debug scripts)
│   └── __init__.py
│
├── skills/                            # Claude Code skills (CLI extensions)
│   ├── health_sync.py
│   ├── log_diet_simple.py
│   ├── log_fasting_simple.py
│   ├── log_supplement_simple.py
│   └── obsidian_sync.py
│
├── tests/                             # Test suite (20 test files)
│   ├── conftest.py                    # pytest configuration
│   ├── health/
│   │   └── test_analytics.py
│   ├── slack_bot/
│   │   ├── unit/
│   │   │   ├── test_context.py
│   │   │   ├── test_dispatcher.py
│   │   │   ├── test_gemini_json.py
│   │   │   ├── test_tools_registry.py
│   │   ├── scenarios/
│   │   │   └── test_e2e.py
│   │   └── test_shell_tools.py
│   ├── unit/
│   │   ├── test_tools_schema.py
│   │   └── test_gemini_resilience.py
│   ├── integration/
│   │   ├── test_cli_scenarios.py
│   │   └── test_live_brain.py
│   └── (more test files)
│
├── data/                              # Runtime data directory
│   ├── health/
│   │   ├── health.db                  # SQLite database (WAL mode enabled)
│   │   ├── daily_metrics/             # JSON: sleep, steps, heart_rate, hrv, stress, etc.
│   │   ├── activities/                # JSON: exercise activities
│   │   ├── body_metrics/              # JSON: weight, BMI
│   │   ├── manual_logs/               # JSON: diet, alcohol, supplements, feelings
│   │   ├── obsidian_context/          # Obsidian sync state
│   │   ├── slack_context/             # Channel conversation context
│   │   └── bot_pids.json              # Running bot process IDs
│   └── proxy_health_history.jsonl     # Proxy health monitoring
│
├── logs/                              # Application logs
│   ├── slack_bot.log
│   ├── health.log
│   ├── health_sync.log
│   ├── shell.log
│   ├── obsidian.log
│   └── weekly_report.log
│
├── docs/                              # Documentation
│   ├── ARCHITECTURE.md                # (to be written)
│   ├── TOOLS_REFERENCE.md             # (to be written)
│   ├── HEALTH_SYNC_README.md
│   ├── context_management.md
│   ├── enhanced_logging_guide.md
│   ├── proxy_decision_guide.md
│   ├── CRON_SETUP.md
│   ├── MANUAL_ENTRY.md
│   ├── TODO.md
│   └── (other docs)
│
├── requirements.txt                   # Python dependencies
├── .env.example                       # Environment variable template
├── .env                               # (not in repo, user-specific)
└── (archived tarballs & config snapshots)
```

---

## 2. CORE ARCHITECTURE LAYERS

### **Layer 1: Data Management (health/ module)**

**Purpose**: Manage and synchronize health data from Garmin Connect.

**Key Components**:
- **HealthDataSync** (`data_sync.py`): Orchestrator
  - Methods: `authenticate()`, `sync_daily_metric()`, `sync_all_metrics()`, `sync_activities()`
  - Handles retry logic (MAX_RETRIES=3, exponential backoff)

- **GarminHealthClient** (`garmin_client.py`): API wrapper
  - 14 fetch methods: `fetch_sleep_data()`, `fetch_steps_data()`, `fetch_heart_rates()`, `fetch_hrv_data()`, `fetch_stress_data()`, `fetch_body_battery()`, `fetch_spo2_data()`, `fetch_respiration_data()`, `fetch_hydration_data()`, `fetch_floors()`, `fetch_intensity_minutes()`, `fetch_rhr_day()`, `fetch_activities_by_date()`, `fetch_weigh_ins()`
  - Token-based authentication with persistence

- **HealthStorage** (`storage.py`): File-based persistence
  - Stores metrics in JSON: `/data/health/{metric_type}/{YYYY}/{MM}/{DD}.json`
  - Methods: `load_daily_metric()`, `save_daily_metric()`, `metric_exists()`, `load_activity()`, `save_activity()`

- **HealthRepository** (`repository.py`): Database access layer
  - CRUD for sync_records, last_sync_state, daily_metrics_index, activity_index
  - SQLite with WAL mode for concurrent access

- **HealthDataQuery** (`query.py`): Query service
  - Methods: `get_daily_summary()`, `get_metric_range()`, `get_activities_by_date_range()`, `get_activities_range()`
  - Combines JSON storage + activity indexing

**Data Models**:
- `DailyMetrics`: sleep, heart_rate, hrv, stress, body_battery, spo2, etc. (Pydantic)
- `Activity`: type, start_time, duration, distance, calories, avg_heart_rate, etc.
- `ManualLog`: diet entries, alcohol, supplements, body feelings, fasting mode
- `SyncMetadata`: last sync state, record counts

**Configuration** (`config.py`):
```python
DATA_TYPE_CONFIG = {
    "steps": {"api_method": "get_steps_data", "storage_path": "daily_metrics/steps"},
    "sleep": {"api_method": "get_sleep_data", "storage_path": "daily_metrics/sleep"},
    "heart_rate": {"api_method": "get_heart_rates", "storage_path": "daily_metrics/heart_rate"},
    "hrv": {"api_method": "get_hrv_data", "storage_path": "daily_metrics/hrv"},
    "stress": {"api_method": "get_stress_data", "storage_path": "daily_metrics/stress"},
    "body_battery": {"api_method": "get_body_battery", "storage_path": "daily_metrics/body_battery"},
    "spo2": {"api_method": "get_spo2_data", "storage_path": "daily_metrics/spo2"},
    "respiration": {"api_method": "get_respiration_data", "storage_path": "daily_metrics/respiration"},
    "hydration": {"api_method": "get_hydration_data", "storage_path": "daily_metrics/hydration"},
    "floors": {"api_method": "get_floors", "storage_path": "daily_metrics/floors"},
    "intensity_minutes": {"api_method": "get_intensity_minutes", "storage_path": "daily_metrics/intensity_minutes"},
    "rhr": {"api_method": "get_rhr_day", "storage_path": "daily_metrics/rhr"},
    "activities": {"api_method": "get_activities_by_date", "storage_path": "activities", "batch_by_date_range": True},
    "weight": {"api_method": "get_weigh_ins", "storage_path": "body_metrics/weight"}
}
```

---

### **Layer 2: Slack Bot (slack_bot/ module)**

**Architecture Pattern**: Message Dispatcher → Tool Registry → LLM Integration

#### **2A. Main Bot Entry Point** (`main.py`)

**Responsibilities**:
- Initialize Slack Bot with SocketModeHandler
- Set up message deduplication
- Command handlers: `/clear`, `/stats`, `/reset`
- Message handler delegation to MessageDispatcher
- Graceful error handling and logging

**Key Classes**:
- `App` (from slack-bolt): Slack application instance

**Command Handlers**:
- `/clear` or `/reset`: Clear conversation history for channel
- `/stats`: Show context statistics (message count, token estimate)

---

#### **2B. Message Dispatcher** (`dispatcher.py`, 580+ lines)

**Core Flow**:
1. **Deduplication**: Skip duplicate messages using `client_msg_id` or `ts`
2. **Context Management**: Load/save conversation history per channel
3. **File Download**: Process uploaded images (resize, compress to JPEG)
4. **LLM Call #1**: Call Gemini with context + tools (or without tools if image download failed)
5. **Safety Override**: If LLM doesn't trigger tools, manually detect intent and force tool calls
6. **Tool Execution**: Run requested tools with error handling
7. **LLM Call #2**: Request final analysis with tool results
8. **Response Formatting**: Apply Slack MarkDown, split into chunks (max 3800 chars each)
9. **Context Persistence**: Save assistant response to channel history

**Key Methods**:
- `dispatch(message_text, channel_id, user_id, response_ts, request_id, files)`: Main handler
- `_download_files(files, prefix)`: Download + optimize images from Slack
- `_format_response(text, tool_results, max_length)`: Format with tool results
- `_split_text(text, max_chunk_size)`: Split long responses into chunks
- `_download_files()`: Image processing (PIL, JPEG conversion)

**Safety Features**:
- **Safety Override (Lines 117-244)**: Manually trigger tools if LLM fails:
  - Sync Garmin: detect "sync", "update", "fetch" + "garmin", "data"
  - Query health: detect sleep, steps, heart rate, activity keywords
  - Parse time ranges: "past X days/weeks/months"
  - Web search: detect "搜索", "search", "latest"
  - Diet logging: detect food items + meal indicators
- **Image validation**: Check for HTML login pages (auth failures)
- **Response truncation**: Max 12,000 chars (3 messages × 3,800 chars)
- **Tool result truncation**: Max 8,000 chars per tool result
- **Error messaging**: Fallback responses for empty/failed generations

**Tool Integration**:
```python
dispatcher = MessageDispatcher(tool_mode="light")  # or "standard", "full", "all"
dispatcher.dispatch(message_text, channel_id, user_id, response_ts, request_id, files)
```

---

#### **2C. LLM Integration** (`llm/gemini.py`)

**Class**: `GeminiLLM`

**Methods**:
- `generate_response(message, context, tools, images)`: Call Gemini API
  - Supports function calling (tool invocation)
  - Image processing (base64 encoded)
  - Streaming support (via generators)
  - Error handling: timeouts, malformed responses, empty responses

**Key Features**:
- **System Instruction**: Loaded from `get_system_instruction()` → includes health.md profile
- **Anti-Hallucination Rules**:
  1. Tool-first policy: always call tools for current data
  2. No data invention: report "No data found" instead of guessing
  3. Vision limitations: admit when images are unclear
  4. Explicit uncertainty: call tools to verify
  5. Action commands: sync, update, fetch must call tools
- **Model Configuration**: Supports `GEMINI_MODEL` env var (default: gemini-2.0-flash-exp)
- **Proxy Support**: Can use OpenRouter as proxy (via `GEMINI_BASE_URL`)
- **Tool Schemas**: Converts Pydantic models to OpenAI tool format

---

#### **2D. Context Storage** (`context/storage.py`)

**Class**: `ContextStorage(channel_id)`

**Purpose**: Persistent per-channel conversation history

**Storage**:
- Location: `/data/health/slack_context/{channel_id}.json`
- Format: `[{"role": "user", "content": "...", "timestamp": ..., "model": ...}, ...]`
- Sliding window: Keep only last 40 messages (MAX_MESSAGES)

**Key Methods**:
- `add_message(role, content, model)`: Append to history + apply compression
- `get_context()`: Return formatted messages for LLM
- `clear()`: Remove all history for channel

**Memory Management**:
- Sliding window prevents unbounded growth
- Compressor (compressor.py) available for aggressive compression if needed

---

### **Layer 3: Tool System (slack_bot/tools/)**

**Architecture**: Registry → Tool Groups → Tool Functions

#### **3A. Tool Registry** (`registry.py`, 21 tools)

**TOOLS_SCHEMA**: List of 21 tool definitions in OpenAI format
```python
{
    "type": "function",
    "function": {
        "name": "tool_name",
        "description": "...",
        "parameters": {
            "type": "object",
            "properties": {...},
            "required": [...]
        }
    }
}
```

**TOOL_FUNCTIONS**: Dictionary mapping tool names to callable functions

**Tools by Category**:

| Category | Count | Tools |
|----------|-------|-------|
| **Health Read** | 4 | `get_daily_detailed_stats`, `get_metric_history`, `get_activity_history`, `get_manual_history`, `get_aggregated_analysis` |
| **Health Write** | 5 | `log_diet`, `log_alcohol`, `log_supplement`, `log_feeling`, `log_fasting` |
| **Analytics** | 2 | `get_health_insights`, `analyze_driver` |
| **Sync** | 2 | `sync_garmin`, `sync_obsidian` |
| **Web** | 1 | `search_web` |
| **Shell** | 1 | `execute_shell` |
| **Files** | 2 | `download_file`, `save_file` |
| **System** | 3 | `get_system_status`, `kill_process`, `get_process_list` |
| **Total** | **21** | |

---

#### **3B. Tool Groups** (`groups.py`)

**Presets** for different proxy/model capabilities:

1. **Light Mode** (6 tools): For proxies with tool limitations
   - `get_daily_detailed_stats`, `get_metric_history`, `log_diet`, `log_supplement`, `sync_garmin`, `search_web`

2. **Standard Mode** (10 tools): Default balanced set
   - Light + `get_activity_history`, `get_manual_history`, `log_alcohol`, `log_feeling`, `get_health_insights`

3. **Full Mode** (15 tools):
   - Standard + `analyze_driver`, `search_web`, `sync_obsidian`, `log_fasting`, `get_aggregated_analysis`

4. **All Mode** (21 tools): Complete set
   - Full + Shell, File, System tools

**Selection** via `TOOL_MODE` env var in `main.py`

---

#### **3C. Tool Implementations**

**HealthReader** (`health_read.py`):
```python
@staticmethod
def get_daily_detailed_stats(target_date: str) -> str
@staticmethod
def get_metric_history(metric_type: str, start_date: str, end_date: str) -> str
@staticmethod
def get_activity_history(start_date: str, end_date: str) -> str
@staticmethod
def get_manual_history(start_date: str, end_date: str, category: str) -> str
@staticmethod
def get_aggregated_analysis(start_date: str = None, end_date: str = None, metrics: List[str] = None) -> str
```

**HealthWriter** (`health_write.py`):
```python
@staticmethod
def log_diet(target_date: str, description: str, meal_type: str = "snack", time: str = None) -> str
@staticmethod
def log_alcohol(target_date: str, alcohol_type: str, amount: str, time: str = None) -> str
@staticmethod
def log_supplement(target_date: str, name: str, dosage: str, time: str = None) -> str
@staticmethod
def log_feeling(target_date: str, feeling_type: str, severity: int, description: str, time: str = None) -> str
@staticmethod
def log_fasting(target_date: str, mode: str) -> str
```

**GarminTool** (`garmin.py`):
```python
@staticmethod
def sync_garmin(target_date: str = None) -> str
```

**AnalyticsTool** (`analytics.py`):
```python
@staticmethod
def get_health_insights(analysis_type: str, lookback_days: int = 30) -> str
@staticmethod
def analyze_driver(driver_metric: str, target_metric: str, method: str, lookback_days: int = 30) -> str
```

**WebSearchTool** (`web.py`):
```python
@staticmethod
def search_web(query: str) -> str
```

**Shell** (`shell_tool.py`):
```python
def execute_shell(command: str, timeout: float = 2.0, channel_id: str = None) -> str
```

**System** (`system_ops.py`, `process_ops.py`):
```python
def get_system_status() -> str
def kill_process(pid: int, signal_type: str = "SIGTERM") -> str
def get_process_list(filter_name: str = None, limit: int = 15) -> str
```

**Files** (`file_ops.py`):
```python
def download_file(file_path: str) -> str
def save_file(file_path: str, content: str, overwrite: bool = False) -> str
```

---

## 3. DATA FLOW DIAGRAMS

### **Scenario 1: User queries sleep data**

```
User (Slack)
  ↓ "How did I sleep last night?"
  ↓
main.py: handle_message()
  ↓ deduplication, file download
  ↓
dispatcher.dispatch()
  ↓
ContextStorage: load conversation history (40 msg max)
  ↓
GeminiLLM.generate_response()
  ├ System Instruction (anti-hallucination rules)
  ├ Context messages
  ├ TOOLS_SCHEMA (21 tools)
  ├ Image data (if any)
  └→ Gemini API (with tools)

  ↓ (Gemini triggers tool)
  ↓
dispatcher: Safety Override (if needed)
  ├ Detect "sleep", "last night" keywords
  ├ Map to get_daily_detailed_stats(target_date="2024-01-22")
  └→ (or let Gemini trigger it naturally)

  ↓ (Tool execution)
  ↓
HealthReader.get_daily_detailed_stats("2024-01-22")
  ├ HealthDataQuery.get_daily_summary("2024-01-22")
  │  ├ HealthStorage.load_daily_metric("sleep", "2024-01-22")
  │  │  └ /data/health/daily_metrics/sleep/2024/01/22.json
  │  ├ HealthRepository.get_activities_by_date_range()
  │  │  └ Activity index query from SQLite
  │  └ ManualLogStorage.load_log("2024-01-22")
  │     └ /data/health/manual_logs/2024/01/22.json
  └→ Formatted string with metrics, activities, manual logs

  ↓ (LLM Call #2: Final analysis)
  ↓
GeminiLLM.generate_response()
  ├ Analysis prompt: "Based on tool results, answer user's question"
  ├ Tool results injected into context
  └→ Natural language response

  ↓ (Format & send)
  ↓
dispatcher._format_response()
  ├ Tool results section
  ├ Slack MarkDown formatting
  └ Split into ≤3 chunks

  ↓
SlackWebClient.chat_update() + chat_postMessage()
  └→ Update "Thinking..." message, post continuations

  ↓
ContextStorage.add_message("assistant", full_response)
  └ Save to /data/health/slack_context/{channel_id}.json
```

---

### **Scenario 2: User logs diet and requests sync**

```
User: "I ate chicken and rice for lunch. Also sync latest Garmin data."

dispatcher.dispatch()
  ↓
ContextStorage: load history
  ↓
GeminiLLM.generate_response()
  ├ Detects: "ate", "chicken", "rice", "lunch", "sync", "garmin"
  ├ Should trigger: log_diet + sync_garmin
  └→ Gemini API (with tools)

  ↓ (LLM might return empty for gemini-3-flash)
  ↓
dispatcher: Safety Override
  ├ Detect diet pattern: has_ate_pattern + has_food_item
  │  └ Force: log_diet(description="chicken and rice", meal_type="lunch", target_date="2024-01-23")
  ├ Detect sync: "sync" + "garmin"
  │  └ Force: sync_garmin(target_date=None → today)
  └→ Execute both tools

  ↓ (Tool #1: log_diet)
  ↓
HealthWriter.log_diet()
  ├ Create DietEntry(time="12:30", description="chicken and rice", meal_type="lunch")
  ├ ManualLogStorage.add_diet_entry()
  │  └ Load /data/health/manual_logs/2024/01/23.json
  │  └ Append entry
  │  └ Save back
  └→ "✅ Logged diet for 2024-01-23: chicken and rice"

  ↓ (Tool #2: sync_garmin)
  ↓
GarminTool.sync_garmin()
  ├ HealthDataSync.authenticate()
  │  └ GarminHealthClient.authenticate()
  ├ HealthDataSync.sync_all_metrics(start=2024-01-23, end=2024-01-23, force=True)
  │  ├ For each metric type in DATA_TYPE_CONFIG:
  │  │  ├ sync_daily_metric("sleep", date)
  │  │  │  ├ GarminHealthClient.fetch_sleep_data(date)
  │  │  │  ├ HealthStorage.save_daily_metric("sleep", date, data)
  │  │  │  └ HealthRepository.upsert_daily_metrics_index()
  │  │  ├ sync_daily_metric("steps", date)
  │  │  ├ sync_daily_metric("heart_rate", date)
  │  │  └ ... (14 metrics total)
  │  ├ sync_activities(start, end)
  │  │  ├ GarminHealthClient.fetch_activities_by_date(date)
  │  │  ├ HealthStorage.save_activity(activity)
  │  │  └ HealthRepository.upsert_activity_index()
  │  └ HealthRepository.upsert_last_sync_state()
  └→ "✅ Synced Garmin data for 2024-01-23: 7 metrics, 2 activities"

  ↓ (LLM Call #2: Final analysis)
  ↓
Tool results injected:
  ├ Tool 'log_diet' returned: "✅ Logged diet for 2024-01-23: chicken and rice"
  └ Tool 'sync_garmin' returned: "✅ Synced Garmin data for 2024-01-23: 7 metrics, 2 activities"

GeminiLLM.generate_response()
  ├ Analysis prompt with tool results
  └→ "Perfect! I've logged your lunch (chicken and rice) and synced your latest Garmin data..."

  ↓
dispatcher._format_response()
  ├ Slack MarkDown formatting
  ├ Split into chunks
  └→ Send to channel

  ↓
ContextStorage.add_message("assistant", full_response)
```

---

### **Scenario 3: User analyzes health trends**

```
User: "过去30天我的睡眠和酒精摄入之间有什么关系?"
(What's the relationship between my sleep and alcohol intake in the past 30 days?)

dispatcher.dispatch()
  ↓
GeminiLLM.generate_response()
  ├ Should trigger: analyze_driver(
  │   driver_metric="alcohol_units",
  │   target_metric="sleep_score",
  │   method="correlation",
  │   lookback_days=30
  │ )
  └→ Gemini API (with tools)

  ↓
dispatcher: Execute tool
  ↓
AnalyticsTool.analyze_driver()
  ├ HealthDataQuery.get_metric_range("alcohol", start, end)
  │  └ Load manual logs for category="alcohol"
  │  └ Extract alcohol_units
  ├ HealthDataQuery.get_metric_range("sleep", start, end)
  │  └ Load JSON files from /data/health/daily_metrics/sleep/
  │  └ Extract sleep_score
  └ Compute correlation via analytics/engine.py:
     ├ Merge datasets on date
     ├ Calculate Pearson correlation coefficient
     ├ Generate statistics (mean, std, percentile)
     └→ "Correlation: -0.68 (negative). On days with alcohol, sleep -12pts"

  ↓
GeminiLLM.generate_response()
  ├ Analysis prompt with correlation results
  └→ Natural language interpretation with advice

  ↓
Send to Slack
```

---

## 4. KEY DESIGN PATTERNS

### **Pattern 1: Service-Oriented Architecture**

Each module has a clear responsibility:
- **GarminHealthClient**: API integration only
- **HealthStorage**: File I/O only
- **HealthRepository**: Database CRUD only
- **HealthDataSync**: Orchestration (combines all above)
- **HealthDataQuery**: Read-only queries
- **ManualLogStorage**: Manual log persistence

**Dependency Injection**: Services accept optional dependencies
```python
class HealthDataSync:
    def __init__(self, client=None, storage=None, repo=None):
        self.client = client or GarminHealthClient()
        self.storage = storage or HealthStorage()
        self.repo = repo or HealthRepository()
```

---

### **Pattern 2: Tool-First LLM Interaction**

1. **System Instruction** drives tool usage (anti-hallucination)
2. **Tool Registry** provides schema for LLM
3. **Safety Override** fallback when LLM fails
4. **Two-Stage Generation**: LLM call → Tool execution → Final response

**Anti-Hallucination** keywords in system prompt:
- "TOOL-FIRST POLICY"
- "NO DATA INVENTION"
- "VISION LIMITATIONS"
- "EXPLICIT UNCERTAINTY"

---

### **Pattern 3: Stateful Conversation Management**

- Per-channel context files (JSON)
- Sliding window (40 messages max)
- Metadata per message (role, timestamp, model)
- Clear command to reset context

**Example**:
```json
[
  {"role": "user", "content": "...", "timestamp": 1698765432.1},
  {"role": "assistant", "content": "...", "timestamp": 1698765433.5, "model": "gemini"}
]
```

---

### **Pattern 4: Incremental Data Sync**

- Track last sync date per metric type
- Fetch only new data (date > last_sync_date)
- Batch processing for historical backfill
- Retry logic with exponential backoff

```python
last_sync = repo.get_last_sync_state("sleep")
if last_sync:
    start_date = last_sync.last_sync_date + timedelta(days=1)
else:
    start_date = HISTORICAL_START_DATE
```

---

## 5. KEY CLASSES & METHODS SUMMARY

### **health/ Module**

| Class | File | Key Methods |
|-------|------|-------------|
| `HealthDataSync` | `data_sync.py` | `authenticate()`, `sync_daily_metric()`, `sync_all_metrics()`, `sync_activities()`, `sync_range()` |
| `GarminHealthClient` | `garmin_client.py` | `authenticate()`, `fetch_sleep_data()`, `fetch_steps_data()`, `fetch_heart_rates()`, `fetch_hrv_data()`, `fetch_stress_data()`, `fetch_body_battery()`, `fetch_spo2_data()`, `fetch_respiration_data()`, `fetch_hydration_data()`, `fetch_floors()`, `fetch_intensity_minutes()`, `fetch_rhr_day()`, `fetch_activities_by_date()`, `fetch_weigh_ins()` |
| `HealthStorage` | `storage.py` | `load_daily_metric()`, `save_daily_metric()`, `metric_exists()`, `load_activity()`, `save_activity()`, `activity_exists()` |
| `HealthRepository` | `repository.py` | `create_sync_record()`, `get_sync_records()`, `upsert_last_sync_state()`, `get_last_sync_state()`, `upsert_daily_metrics_index()`, `get_daily_metric_record()`, `upsert_activity_index()`, `get_activities_by_date_range()` |
| `HealthDataQuery` | `query.py` | `get_daily_summary()`, `get_metric_range()`, `get_activities_by_date_range()`, `get_activities_range()` |
| `ManualLogStorage` | `manual_log_storage.py` | `add_diet_entry()`, `add_alcohol_entry()`, `add_supplement_entry()`, `add_feeling_entry()`, `load_log()`, `save_log()` |

### **slack_bot/ Module**

| Class | File | Key Methods |
|-------|------|-------------|
| `MessageDispatcher` | `dispatcher.py` | `dispatch()`, `_download_files()`, `_format_response()`, `_split_text()` |
| `GeminiLLM` | `llm/gemini.py` | `generate_response()`, `get_model_name()` |
| `ContextStorage` | `context/storage.py` | `add_message()`, `get_context()`, `clear()` |
| `HealthReader` | `tools/health_read.py` | `get_daily_detailed_stats()`, `get_metric_history()`, `get_activity_history()`, `get_manual_history()`, `get_aggregated_analysis()` |
| `HealthWriter` | `tools/health_write.py` | `log_diet()`, `log_alcohol()`, `log_supplement()`, `log_feeling()`, `log_fasting()` |
| `AnalyticsTool` | `tools/analytics.py` | `get_health_insights()`, `analyze_driver()` |
| `GarminTool` | `tools/garmin.py` | `sync_garmin()` |

---

## 6. TOOL FILES DIRECTORY STRUCTURE

**All files in `/root/projects/butler/slack_bot/tools/`:**

1. **registry.py** (382 lines)
   - TOOLS_SCHEMA: 21 tool definitions
   - TOOL_FUNCTIONS: Mapping of tool names to callables
   - Comprehensive function schema documentation

2. **groups.py** (71 lines)
   - LIGHT_TOOLS: 6 tools (proxy-safe)
   - STANDARD_TOOLS: 10 tools
   - FULL_TOOLS: 15 tools
   - `get_tool_preset(mode)`: Return tool set for mode

3. **health_read.py** (200+ lines)
   - HealthReader class (5 static methods)
   - Tools: get_daily_detailed_stats, get_metric_history, get_activity_history, get_manual_history, get_aggregated_analysis

4. **health_write.py** (150+ lines)
   - HealthWriter class (5 static methods)
   - Tools: log_diet, log_alcohol, log_supplement, log_feeling, log_fasting

5. **analytics.py** (150+ lines)
   - AnalyticsTool class (2 static methods)
   - Tools: get_health_insights, analyze_driver
   - Correlation analysis, trend detection

6. **garmin.py** (80+ lines)
   - GarminTool class (1 static method)
   - Tool: sync_garmin
   - Wraps HealthDataSync for tool interface

7. **web.py** (50+ lines)
   - WebSearchTool class (1 static method)
   - Tool: search_web
   - DuckDuckGo or external search integration

8. **obsidian.py** (100+ lines)
   - ObsidianTool class (1 static method)
   - Tool: sync_obsidian
   - Vault sync and note management

9. **shell_tool.py** (150+ lines)
   - Function: execute_shell()
   - Tool: execute_shell
   - Persistent shell session management
   - Command blacklist for safety (rm, mv, git add, etc.)

10. **file_ops.py** (100+ lines)
    - Functions: download_file(), save_file()
    - Tools: download_file, save_file
    - File upload to Slack, file creation

11. **system_ops.py** (80+ lines)
    - Function: get_system_status()
    - Tool: get_system_status
    - CPU, Memory, Disk usage dashboard

12. **process_ops.py** (100+ lines)
    - Functions: kill_process(), get_process_list()
    - Tools: kill_process, get_process_list
    - Process management

**Total**: 12 files, ~1,500 lines of tool implementation code

---

## 7. CONFIGURATION & ENVIRONMENT

### **Key Environment Variables**

```bash
# Slack Bot
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...

# Gemini/LLM
GEMINI_MODEL=gemini-2.0-flash-exp
GEMINI_BASE_URL=https://openrouter.ai/api  (optional, for proxy)
GEMINI_API_KEY=...

# Garmin
GARMIN_EMAIL=...
GARMIN_PASSWORD=...

# Tool Mode
TOOL_MODE=light|standard|full|all  (default: light)

# Features
SHELL_BOT_ENABLED=true|false  (default: false)
TOOL_MODE=all  (for Health Bot)

# Slack Reporting
SLACK_REPORT_CHANNEL=C0XXXXXXXXX  (for weekly reports)
```

### **Data Paths**

```python
PROJECT_ROOT = Path(__file__).parent.parent  # /root/projects/butler
DATA_DIR = PROJECT_ROOT / "data" / "health"  # /root/projects/butler/data/health
DB_PATH = DATA_DIR / "health.db"
LOGS_DIR = PROJECT_ROOT / "logs"
TOKEN_STORE = Path.home() / ".garmin" / "tokens"
```

---

## 8. DATABASE SCHEMA

### **SQLite Tables** (health.db)

**1. sync_records**
```sql
CREATE TABLE IF NOT EXISTS sync_records (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    data_type TEXT NOT NULL,
    start_date TEXT NOT NULL,
    end_date TEXT NOT NULL,
    status TEXT NOT NULL,  -- 'success', 'failed', 'partial'
    records_synced INTEGER DEFAULT 0,
    error_message TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

**2. last_sync_state**
```sql
CREATE TABLE IF NOT EXISTS last_sync_state (
    data_type TEXT PRIMARY KEY,
    last_sync_date TEXT NOT NULL,
    total_records INTEGER DEFAULT 0,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

**3. daily_metrics_index**
```sql
CREATE TABLE IF NOT EXISTS daily_metrics_index (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    metric_type TEXT NOT NULL,
    date TEXT NOT NULL,
    file_path TEXT NOT NULL,
    has_data BOOLEAN DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(metric_type, date)
);
```

**4. activity_index**
```sql
CREATE TABLE IF NOT EXISTS activity_index (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    activity_id TEXT UNIQUE NOT NULL,
    activity_type TEXT,
    date TEXT NOT NULL,
    duration_seconds INTEGER,
    distance_meters REAL,
    file_path TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

**WAL Mode**: Enabled for concurrent access

---

## 9. FILE STORAGE STRUCTURE

### **Daily Metrics** (JSON)

```
/data/health/daily_metrics/
├── sleep/
│   └── 2024/01/23.json
├── steps/
│   └── 2024/01/23.json
├── heart_rate/
│   └── 2024/01/23.json
├── hrv/
│   └── 2024/01/23.json
├── stress/
│   └── 2024/01/23.json
├── body_battery/
│   └── 2024/01/23.json
└── ... (14 metric types total)
```

**Example: sleep/2024/01/23.json**
```json
{
  "date": "2024-01-23",
  "sleep_score": 82,
  "total_sleep_seconds": 27120,
  "deep_sleep_seconds": 6300,
  "rem_sleep_seconds": 7200,
  "light_sleep_seconds": 13620,
  "awake_seconds": 1800,
  "start_time": "2024-01-22T23:30:00Z",
  "end_time": "2024-01-23T07:22:00Z"
}
```

### **Activities**

```
/data/health/activities/
└── 2024/01/23.json
```

**Example: activities/2024/01/23.json**
```json
[
  {
    "activity_id": "315781457",
    "activity_type": "Running",
    "start_time": "2024-01-23T06:00:00Z",
    "duration_seconds": 1800,
    "distance_meters": 5000,
    "calories": 450,
    "average_heart_rate": 145,
    "max_heart_rate": 165,
    "min_heart_rate": 110
  }
]
```

### **Manual Logs**

```
/data/health/manual_logs/
└── 2024/01/23.json
```

**Example: manual_logs/2024/01/23.json**
```json
{
  "date": "2024-01-23",
  "diet_entries": [
    {"time": "08:00", "description": "Oatmeal with berries", "meal_type": "breakfast"}
  ],
  "alcohol_entries": [
    {"time": "19:00", "alcohol_type": "Red wine", "amount": "200ml"}
  ],
  "supplement_entries": [
    {"time": "07:00", "supplement_name": "Magnesium", "dosage": "400mg"}
  ],
  "feeling_entries": [
    {"time": "15:00", "feeling_type": "Headache", "severity": 5, "description": "Mild afternoon headache"}
  ],
  "fasting_mode": null
}
```

### **Slack Context**

```
/data/health/slack_context/
└── C123456789.json  (per channel ID)
```

---

## 10. TESTING STRUCTURE

### **Test Files** (20+ files)

```
tests/
├── conftest.py                    # pytest configuration + fixtures
├── health/
│   └── test_analytics.py         # Analytics engine tests
├── slack_bot/
│   ├── unit/
│   │   ├── test_context.py       # ContextStorage tests
│   │   ├── test_dispatcher.py    # MessageDispatcher tests
│   │   ├── test_gemini_json.py   # Gemini response parsing
│   │   ├── test_tools_registry.py # Tool registration
│   ├── scenarios/
│   │   └── test_e2e.py           # End-to-end scenarios
│   └── test_shell_tools.py       # Shell tool tests
├── unit/
│   ├── test_tools_schema.py      # Tool schema validation
│   ├── test_gemini_resilience.py # LLM resilience
├── integration/
│   ├── test_cli_scenarios.py     # CLI integration tests
│   └── test_live_brain.py        # Live integration tests
├── test_context_commands.py       # /clear, /stats commands
├── test_garmin_client_new_features.py
└── test_mrkdwn.py                # MarkDown formatting
```

**Run Tests**:
```bash
pytest tests/
pytest tests/slack_bot/unit/test_dispatcher.py -v
pytest tests/integration/ -v
```

---

## 11. EXECUTION FLOW SUMMARY

### **Bot Startup**

```bash
python slack_bot/main.py --model gemini-2.0-flash-exp

1. Load environment (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, GEMINI_API_KEY, etc.)
2. Initialize Slack App (socket mode)
3. Create MessageDispatcher with tool_mode from env
4. Register message handlers (deduplication, commands)
5. Start SocketModeHandler → Listen for messages
```

### **Message Processing Pipeline**

```
User Message
  ↓
Deduplication (check client_msg_id/ts)
  ↓
File Download (if images attached)
  ↓
ContextStorage.load_context()
  ↓
GeminiLLM.generate_response()
  ├ System Instruction + Anti-hallucination rules
  ├ Context (last 40 messages)
  ├ TOOLS_SCHEMA (or tool_mode preset)
  ├ Images (if any, base64 encoded)
  └→ Gemini API

Response (text + tool_calls)
  ↓
Safety Override (if no tool_calls)
  └ Keyword detection to trigger tools

  ↓
Tool Execution (sequential)
  └ Each tool collects results

  ↓
LLM Call #2 (Analysis)
  ├ Tool results injected as context
  └→ Final response text

  ↓
Format & Send
  ├ Slack MarkDown conversion
  ├ Split into ≤3 chunks (3,800 chars each)
  ├ Update "Thinking..." message
  └→ Post continuations

  ↓
ContextStorage.add_message()
  └ Save assistant response to channel history
```

---

## 12. KEY METRICS & STATISTICS

- **Total Python Files**: 424 (excluding venv, tests, __pycache__)
- **Core Code**: ~10,600 lines
- **Test Files**: 20+
- **Health Metrics**: 14+ types
- **AI Tools**: 21 tools (6 light, 10 standard, 15 full, 21 all)
- **Database Tables**: 4
- **Tool Implementations**: 12 files, ~1,500 lines
- **Data Storage**: 104 MB (SQLite + JSON)
- **Supported Languages**: English + Chinese (Simplified)
- **Slack Features**: Context management, command handlers, deduplication, image processing
- **Safety Features**: Blacklist for shell commands, image validation, response truncation, anti-hallucination rules

---

## 13. NOTABLE DESIGN DECISIONS

1. **Tool Modes (Light/Standard/Full/All)**
   - Adapts to proxy capabilities (OpenRouter vs Direct Google API)
   - Light mode (6 tools) for proxies with tool limitations
   - All mode (21 tools) for full capabilities

2. **Safety Override Mechanism**
   - Fallback when LLM fails to trigger tools
   - Keyword-based detection for common intents
   - Forces tool calls manually for critical operations

3. **Two-Stage LLM Generation**
   - Stage 1: Get LLM response + tool calls
   - Tool Execution: Run tools, collect results
   - Stage 2: Request final analysis with tool results
   - Improves response quality and reduces hallucinations

4. **Per-Channel Conversation Context**
   - Persistent JSON storage
   - Sliding window (40 messages)
   - Allows multi-user interaction per channel
   - Clear command to reset context

5. **Incremental Data Sync**
   - Track last sync date per metric
   - Only fetch new data
   - Batch processing for history
   - Retry logic with exponential backoff

6. **File-Based + Database Hybrid Storage**
   - JSON for human-readable metrics
   - SQLite for fast indexing
   - Complementary approach (not redundant)

7. **Anti-Hallucination System Instruction**
   - Tool-first policy
   - No data invention
   - Vision limitations
   - Explicit uncertainty
   - Action commands must use tools

---

This comprehensive documentation covers the entire Butler architecture, ready for your implementation and documentation needs!