Skip to content

About

Data-analysis AI agent for meal, workout and sleep logs. Gemini interprets and explains, typed tools compute the statistics, and answers come with evidence. It also has RAG over notes, guarded read-only SQL, a small ML predictor, an MCP server, and an EN/JA UI. Synthetic demo data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Python-Health-Insight-Agent

English | 日本語

A data-analysis AI agent over meal, workout and sleep logs. It investigates the data and answers with its evidence (which data, how many records, which statistics): Gemini interprets and explains, typed tools compute, and a small custom model handles predictions.

This is a demo on synthetic data. No real personal data is included in the code, the evaluation or any screenshot. Answers are informational, based on logged data, and are not medical diagnosis or treatment advice.

Example questions:

  • "What did my bad-sleep days have in common?" → a comparison tool splits the days and computes the statistics; the LLM only puts the result into words.
  • "Is breakfast size related to my sleep?" → for a relationship that was deliberately not planted in the data, the agent answers that there is none (no invented findings).
  • "What did my notes say on the days I slept badly?" → RAG over reflection notes, quoted with dates.
  • "Average sleep by day of week, using SQL" → guarded, read-only SQL.
  • "If I finish dinner at 22:30 and go to bed at midnight, how will I sleep tomorrow?" → a small custom model returns a reference value with its error (MAE) and its improvement over a baseline; if the model does not meet its adoption criteria it is simply not used.

Statistics appear as plain-language finding cards right under the chart (a strength meter, record count and reliability), and sleep is shown in hours (e.g. 7.5 h) rather than minutes.

The UI, answers, chart labels, finding cards, tool-result texts (health-score findings, model reasons) and the keyless demo LLM all switch between English and Japanese (language selector in the UI, --lang in the CLI, locale in the API; the language also travels over MCP). Your own data (notes, exercise names) stays in the language it was written in, and earlier messages in a chat keep the language they were asked in.

Screenshots

All screenshots use synthetic data and show the English UI (the Japanese UI is shown in README.ja.md). Images 2–4 are real answers from Gemini (gemini-3.6-flash); the start screen does not depend on the model; the prediction card (image 5) was captured with the keyless demo LLM (AI_PROVIDER=fake).

1. Start screen: suggested questions, the language toggle and the New chat button.

Start screen

2. Gemini's answer: the model chose its own tools; light Markdown (bold, bullets) is rendered, sleep is reported in hours, and every number comes from a tool result.

Gemini's answer in the chat

3. The charts its tool calls produced: scatter plots (two clear groups by last-meal time) and bars in hours.

Charts produced by the tool calls

4. Finding cards right below the charts: a strength meter, the correlation r, the number of records and the reliability; then a group comparison table with a verdict per row.

Finding cards and group comparison

5. Prediction from the custom model (keyless demo LLM): always labelled Model-based reference value, with the model's error (MAE) and its improvement over the baseline.

Predicted sleep with the reference-value label

Highlights

Topic What it does Where
Division of labour Gemini only interprets the question, picks tools and explains. Numbers and significance are computed in code; the LLM never calculates ADR 0001
Safe tools user_id is not a tool argument: it is injected from the authenticated context; unknown arguments are rejected ADR 0002
Guarded SQL Parse → allow-list → enforced per-user scoping (AST rewrite) → READ ONLY → timeout → row limit → read-only DB role ADR 0003
MCP All tools are exposed as an MCP server; a setting switches between direct calls and MCP (tests assert identical results) ADR 0004
RAG Semantic search over reflection notes. VectorStore is a Protocol, so pgvector can replace the NumPy store later ADR 0006
LLM + custom model Next-day sleep is predicted with LightGBM. Adopted only if it beats the baseline by ≥10% with a train/validation gap ≤20%; otherwise it falls back to statistics Custom model, ADR 0009, 0010
Readable results Statistics become plain-language finding cards under the chart (strength meter, records, reliability); sleep is shown in hours; UI, answers, charts and tool texts switch between English and Japanese ADR 0011
Evaluation 25 ground-truth questions (finding planted patterns, saying "there is none", facts, RAG, SQL, refusing to diagnose, custom model). Deterministic scoring, no LLM judge Results
Cost control Per-question log of tokens / tool calls / latency; tool-call cap; rate limit; embedding cache; only recent history in the prompt logs/, AgentConfig
Integration Same schema, naming, RetryPolicy and provider switch as the existing app Python-Smart-Health-Intelligence-Platform, which is left untouched ADR 0005

Architecture

flowchart LR
  UI[React chat UI<br/>collapsible evidence · charts] -->|POST /api/chat| API[FastAPI<br/>validation · rate limit]
  CLI[CLI] --> Agent
  API --> Agent[Agent loop<br/>tool-call cap · usage log]
  Agent -->|generate| LLM[(LLMProvider<br/>Gemini / Fake)]
  Agent --> Exec{ToolExecutor}
  Exec -->|direct| Reg[ToolRegistry<br/>Pydantic validation · user_id injection]
  Exec -->|MCP stdio| MCP[MCP server] --> Reg
  Reg --> Svc[Services<br/>day table · stats · HealthScore]
  Reg --> Notes[NoteSearch<br/>VectorStore]
  Reg --> SQL[Guarded SQL]
  Reg --> Model[Sleep model<br/>adoption · fallback]
  Svc --> Repo[Repositories<br/>every query scoped by user_id]
  Repo --> DB[(PostgreSQL)]
  SQL --> DB
  Model --> Svc
  Notes -->|embeddings| Emb[(Gemini / hashing)]
Loading

Layers: Presentation (API/CLI/MCP/tools) → Service → Domain ← Repository → Infrastructure. Pydantic at the boundaries, dataclasses inside.

Setup and run

Prerequisites: Docker, uv, Node 24. uv manages Python 3.15.1.

docker compose up -d                      # PostgreSQL 16 (dev: health_insight / test: health_insight_test)
uv sync
cp .env.example .env && cp .env.test.example .env.test
uv run alembic upgrade head               # dev database
uv run python -m health_insight_agent.synthetic.seed   # 90 days of synthetic data (demo user)

CLI (no key needed; AI_PROVIDER=fake is the default; --lang ja for Japanese):

uv run python -m health_insight_agent.agent.cli --lang en --show-tools "Does eating late affect how long I sleep?"
TOOL_TRANSPORT=mcp uv run python -m health_insight_agent.agent.cli --lang en "Same question, over MCP"

Web UI:

uv run uvicorn health_insight_agent.api.app:create_app --factory --port 8000
cd frontend && npm ci && npm run dev      # http://localhost:5173

The web UI offers: suggested questions, the answer with charts, finding cards under them, a collapsible "Tools used and results" panel (arguments, record counts, raw results), per-answer usage chips (model, tokens, tool calls, latency), the "Model-based reference value" label on predictions, an English / Japanese toggle, a "New chat" button (or a click on the logo) that clears the chat and returns to the start screen, light and dark themes, and a responsive layout.

MCP server (usable from other MCP clients; the user is bound at startup):

MCP_USER_ID=<uuid> uv run python -m health_insight_agent.mcp_server

Tests, lint, types:

uv run ruff check . && uv run ruff format --check . && uv run mypy src && uv run pytest
cd frontend && npm run typecheck && npm test

Tests only ever connect to a dedicated database whose name ends in _test (otherwise the run aborts) and never read .env (your real database).

GitHub Actions (.github/workflows/ci.yml) runs Ruff, ruff format --check, mypy, the Alembic migrations, pytest (against a Postgres 16 service) and an offline evaluation that fails if the keyless baseline drops below 85% accuracy; a second job type-checks, tests and builds the frontend.

Configuration

Everything is configured through environment variables (copy .env.example to .env; .env is git-ignored).

Variable Default Meaning
DATABASE_URL local Docker Postgres (port 5433) Database. The agent only ever reads, inside READ ONLY transactions
READONLY_DATABASE_URL empty Optional. A user without write permission. Empty: the agent uses DATABASE_URL. Set it to read the existing app's database (e.g. Neon) or to enable the DB-role defence of the SQL tool locally (below)
DEMO_USER_ID fixed UUID Stand-in for authentication (ADR 0005)
AI_PROVIDER fake in .env.example (gemini if unset) fake = keyless rule-based demo LLM, gemini = real Gemini
GEMINI_API_KEY – Needed for gemini
GEMINI_MODEL / GEMINI_EMBEDDING_MODEL gemini-3.6-flash / gemini-embedding-2 Models; change without code edits
TOOL_TRANSPORT direct direct calls tools in-process, mcp goes through an MCP server subprocess
SQL_TOOL, ML_TOOL, ML_TRACKING on Feature switches: guarded SQL tool, sleep-model tools, MLflow logging
ML_ARTIFACT_DIR ml_artifacts/ Where adoption-decision JSON is written
CHAT_RATE_LIMIT_PER_MIN 20 Per-client request limit of the API
CORS_ORIGINS http://localhost:5173,… Allowed browser origins for the dev frontend
MCP_USER_ID, MCP_TODAY demo user / today MCP server only: the bound user, and a fixed "today" for evaluation
LOG_LEVEL INFO Logging

Creating a read-only database user (optional, for READONLY_DATABASE_URL). Migration 0002 creates the group role health_insight_reader (SELECT on the log tables only, not on users). Add a login user to it:

READONLY_PASSWORD='choose-a-password' uv run python -m health_insight_agent.database.readonly_role --user agent_ro
# prints: READONLY_DATABASE_URL=postgresql+psycopg://agent_ro:<password>@localhost:5433/health_insight

Put that URL (with your real password) into .env. The role cannot write and cannot read e-mail addresses.

Trying it with a real Gemini key

CI and the tests never call the real API. To try it with your own key:

# in .env
AI_PROVIDER=gemini
GEMINI_API_KEY=...            # only the question, tool results and note text are sent (never e-mail / user_id)
GEMINI_MODEL=                 # empty = gemini-3.6-flash (stable). Embeddings: GEMINI_EMBEDDING_MODEL (default gemini-embedding-2)

uv run python -m health_insight_agent.agent.cli --lang en --show-tools "What did my bad-sleep days have in common?"

Verified with a real key (2026-11-08, gemini-3.6-flash, free tier): function calling with tool results, replaying the thought_signature, 429 handling and embeddings all work (the screenshots above are real runs). The real key found one bug, now fixed: gemini-embedding-2 is multimodal and merges a list of strings into one embedding, so each text is sent as its own Content (768 dimensions). Not yet measured: the full evaluation on Gemini.

Free-tier limits: gemini-3.6-flash allows only 5 requests per minute on the free tier, and one question takes 3–5 requests (each tool round trip is one). Asking quickly therefore hits HTTP 429; the app then says that the rate limit was reached and to wait about a minute (the retry policy only waits 1 s / 2 s, which cannot outlast a per-minute quota). For a smooth experience or for the evaluation, use a paid key, or space the questions out (--pause, below).

Evaluating with a real key (costs tokens/money):

uv run python -m health_insight_agent.evaluation --list
uv run python -m health_insight_agent.evaluation --include-model \
  --config gemini-flash-all --config gemini-flash-no-model --config gemini-flash-minimal-prompt \
  --config gemini-flash-typed-only --config gemini-flash-lite-all \
  --config fake-all --pause 20 --write-docs docs/evaluation-results.md   # --pause: seconds between questions

Evaluation

The synthetic data contains known causal patterns (synthetic/patterns.py):

Planted Effect
Late meal → shorter sleep eating within 3 h of bedtime shortens sleep by about 60 min
Evening high-intensity workout → later bedtime a high-intensity workout after 20:00 delays bedtime by about 50 min (sleep length unchanged)
Shorter sleep → higher fatigue the fatigue score rises that day
(Not planted) breakfast kcal, weight, weekday/weekend, workout kcal and stress are unrelated to sleep duration

These are the ground truth for 25 questions (22 general + 3 custom-model), scored deterministically (tool choice, evidence in the tool results, regexes on the answer, numeric grounding, forbidden phrases). "Fabrication" counts null-pattern questions where the tool said "no relationship" but the answer did not. (The question set itself is in Japanese.)

Results (3 keyless configurations × 25 questions; full tables in docs/evaluation-results.md)

Configuration Accuracy Planted patterns Says "none" Facts Notes RAG SQL Not in data Refuses to diagnose Custom model Fabricated (null) Numeric consistency (model)
fake-all (all tools + custom model) 22/25 (88%) 5/6 6/6 5/5 2/2 1/1 0/1 0/1 3/3 0/6 3/3
fake-no-model (no custom model) 19/25 (76%) 5/6 6/6 5/5 2/2 1/1 0/1 0/1 0/3 0/6 -
fake-typed-only (no RAG / SQL / model) 15/25 (60%) 5/6 5/6 5/5 0/2 0/1 0/1 0/1 0/3 0/6 -

How to read this:

  • fake-* is not an LLM: it is a keyword-driven baseline that picks tools and echoes their results. It exists to show, reproducibly and without a key, that the tool layer (statistics, date alignment, guards, RAG, model) and the scoring work.
  • Custom model on/off: the three "what if I eat late tonight?" questions score 3/3 with the model and 0/3 without. Asking the same question three times returns the same predicted value each time (numeric consistency 3/3). Without the model there is no predicted value at all. This column is meant to expose the risk that an LLM guessing a number varies between runs; measure it on Gemini with the commands above.
  • Removing tools (no RAG/SQL) makes the notes, SQL and weekday-vs-weekend questions fail (tool-configuration comparison).
  • The three questions the rule-based baseline fails (P6 describing the size of a difference in words, U1 saying something is not in the data, D1 declining to diagnose) are exactly what the LLM is for. They also show the scoring does not wave through plausible-sounding text (the fixed disclaimer is excluded from scoring).
  • No Gemini results are in this table (no API key in the development environment, so not measured). Comparisons across prompt (full/minimal), tool configuration, model (Flash/Flash-Lite) and custom model on/off are preset and ready to run with the commands above; append the results to docs/evaluation-results.md once measured.

Custom model (sleep duration prediction)

The tools answer "what happened" with statistics. Only the prediction ("what if I change X?") is done by a small custom model (LightGBM). The LLM never produces a predicted number; it only puts the tool's value and quality metrics into words.

Item Detail
Target Sleep duration (min) of a given morning; metric: MAE
Features Previous day: last meal time, bedtime, the gap between them, total kcal, workout kcal / evening high-intensity workout / intensity; previous night and last-3-nights sleep; weekend
Evaluation Forward-chaining time-series CV (never predicts the past from the future). Baseline = the better of "mean of the last 7 nights" and "training mean"
Adoption criteria ≥10% relative improvement over the baseline and train/validation error gap ≤20% (overfitting check). If either fails, the model is not used
Preconditions ≥60 days of logs and <30% missing. Otherwise evaluation is skipped and it falls back to plain statistics
Days without a workout Treated as 0, not as missing (the existing app once treated a None workout duration as missing and always fell back; regression-tested)
Tools predict_sleep_duration (value + model MAE + baseline ratio), explain_sleep_factors (feature importance; not causal)
Answer contract The value is stated as a reference value with the MAE and baseline ratio; if the model was not adopted the answer says no model was used (the UI also shows a "Model-based reference value" label)
Tracking Adoption decision as JSON (ml_artifacts/); training conditions, metrics and decision in MLflow (mlruns/, local SQLite). Both are git-ignored
uv run python -m health_insight_agent.ml --today 2026-11-07    # train / evaluate / adoption report

Example run on synthetic data (demo user, 89 days):

Data: 89 days / 82 samples / 7.9% missing
MAE: model(val)=21.5 min  model(train)=25.0 min   baseline(train mean)=31.5 / recent-7-nights=33.8
Improvement vs baseline = 31.6% (needs ≥10%)   train/val gap = -16.1% (needs ≤20%)
Decision: adopted   top importance: meal_to_bed_gap_h=0.71, last_meal_hour=0.15, ...

Note: this is a pipeline demonstration on synthetic data, not a claim about predictive accuracy. That the planted "time from last meal to bed" tops the importance list only confirms the pipeline can pick up what was planted. Accuracy on real data is untested. A deliberately overfit-prone setting (150 trees, 7 leaves) reaches train 9 min / val 23 min (gap 60%) on the same data and is rejected by the overfitting check (tests/unit/test_ml.py): a model that merely looks accurate is not used.

Relation to the existing app

The schema (SleepLog / MealLog / 3-level WorkoutLog / 5-axis HealthLog / DailyJournal), package layout (domain / repositories / services / ai / ml), RetryPolicy (3 attempts, 1 s × 2 backoff, ±20% jitter, 429/503 only) and the AI_PROVIDER=fake|gemini switch match the existing app Python-Smart-Health-Intelligence-Platform, which is left unchanged (read-only). HealthScore / Recommendation are ported behind a HealthInsightPort; on integration, swap in an adapter around the existing service. When reading an existing database, only a user without write permission (READONLY_DATABASE_URL) is used. Authentication on integration (REST API + Google OAuth) is an open point in ADR 0005.

Known limitations

  • Validated on synthetic data only; behaviour on real data (gaps, seasonality, routine changes) is untested.
  • Causation is never asserted: the tools return group differences and correlations, without adjusting for confounders.
  • Connectivity with a real Gemini key is verified (see above), but the evaluation on Gemini is not measured; free-tier rate limits make a full run slow.
  • The sleep model is a synthetic-data pipeline demonstration. On real data it may not be adopted because of data volume, gaps or routine changes (it then falls back to statistics). Predictions come from a statistical model that ignores confounders; they are not causal.
  • Delayed sleep onset has no column, so it is represented by a later bedtime; workout intensity has no column, so it is derived from estimated kcal.
  • Authentication is stood in for by a fixed demo user (only current_user_id in api/app.py needs replacing).
  • The rate limiter is per-process (use a shared store for multiple processes).
  • Vector search is an in-memory NumPy store sized for a few thousand notes (swap in pgvector).
  • UI, answers and chart labels switch between English and Japanese; tool output keys are English. The evaluation question set is Japanese only, and the ADRs, requirements and evaluation results are written in Japanese.
  • The time zone is fixed to JST.

Layout

src/health_insight_agent/
  domain/ database/ repositories/ services/   # same names as the existing app; user_id boundary, day table, stats
  ai/            # LLMProvider (Gemini/Fake), RetryPolicy, embeddings
  tools/         # typed tools, registry, chart specs, SQL tool
  readonly_sql/  # SQL guard and executor
  rag/           # VectorStore (Protocol), note search
  ml/            # sleep model (features, CV, adoption, MLflow)
  agent/         # loop, prompts, usage log, CLI
  mcp_server/    # MCP server and client
  api/           # FastAPI
  synthetic/     # synthetic data with known patterns
  evaluation/    # question set, scoring, configuration comparison
frontend/        # React + Vite + TypeScript (SCSS)
docs/            # requirements.md, adr/, evaluation-results.md (Japanese)
ml_artifacts/ mlruns/   # adoption JSON and MLflow (generated, git-ignored)

License

MIT License

About

Data-analysis AI agent for meal, workout and sleep logs. Gemini interprets and explains, typed tools compute the statistics, and answers come with evidence. It also has RAG over notes, guarded read-only SQL, a small ML predictor, an MCP server, and an EN/JA UI. Synthetic demo data.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages