{"slug": "simple-langgraph-bike-training-agent", "title": "Simple LangGraph Bike Training Agent", "summary": "A developer built a LangGraph-based bike training agent that logs workouts, retrieves recent training history, and provides summary statistics. The agent uses a set of tools to manage a JSON-based workout log and is powered by a language model with a cycling coach system prompt.", "body_md": "| import json | |\n| import os | |\n| from datetime import date, datetime | |\n| from pathlib import Path | |\n| from typing import Annotated | |\n| from langchain_litellm import ChatLiteLLM | |\n| from langchain_core.messages import HumanMessage | |\n| from langchain_core.tools import tool | |\n| from langgraph.prebuilt import create_react_agent | |\n| WORKOUTS_FILE = Path(\"workouts.json\") | |\n| def _load_workouts() -> list[dict]: | |\n| if not WORKOUTS_FILE.exists(): | |\n| return [] | |\n| return json.loads(WORKOUTS_FILE.read_text()) | |\n| def _save_workouts(workouts: list[dict]) -> None: | |\n| WORKOUTS_FILE.write_text(json.dumps(workouts, indent=2)) | |\n| @tool | |\n| def log_workout( | |\n| workout_type: Annotated[str, \"Type of workout: ride, interval, recovery, race, etc.\"], | |\n| duration_min: Annotated[int, \"Duration in minutes\"], | |\n| distance_km: Annotated[float | None, \"Distance in kilometers, if applicable\"] = None, | |\n| avg_power_w: Annotated[int | None, \"Average power in watts, if applicable\"] = None, | |\n| avg_hr_bpm: Annotated[int | None, \"Average heart rate in BPM, if applicable\"] = None, | |\n| notes: Annotated[str | None, \"Any notes about the workout\"] = None, | |\n| workout_date: Annotated[str | None, \"Date in YYYY-MM-DD format, defaults to today\"] = None, | |\n| ) -> str: | |\n| \"\"\"Log a completed bike workout.\"\"\" | |\n| entry = { | |\n| \"date\": workout_date or date.today().isoformat(), | |\n| \"type\": workout_type, | |\n| \"duration_min\": duration_min, | |\n| \"distance_km\": distance_km, | |\n| \"avg_power_w\": avg_power_w, | |\n| \"avg_hr_bpm\": avg_hr_bpm, | |\n| \"notes\": notes, | |\n| } | |\n| workouts = _load_workouts() | |\n| workouts.append(entry) | |\n| _save_workouts(workouts) | |\n| return f\"Logged {workout_type} workout on {entry['date']} ({duration_min} min).\" | |\n| @tool | |\n| def get_recent_workouts( | |\n| days: Annotated[int, \"Number of days to look back\"] = 14, | |\n| ) -> str: | |\n| \"\"\"Get recent workouts from the training log.\"\"\" | |\n| workouts = _load_workouts() | |\n| if not workouts: | |\n| return \"No workouts logged yet.\" | |\n| cutoff = date.today().toordinal() - days | |\n| recent = [w for w in workouts if date.fromisoformat(w[\"date\"]).toordinal() >= cutoff] | |\n| if not recent: | |\n| return f\"No workouts in the last {days} days.\" | |\n| lines = [] | |\n| for w in sorted(recent, key=lambda x: x[\"date\"], reverse=True): | |\n| parts = [f\"{w['date']} | {w['type']} | {w['duration_min']} min\"] | |\n| if w.get(\"distance_km\"): | |\n| parts.append(f\"{w['distance_km']} km\") | |\n| if w.get(\"avg_power_w\"): | |\n| parts.append(f\"{w['avg_power_w']}W avg\") | |\n| if w.get(\"avg_hr_bpm\"): | |\n| parts.append(f\"{w['avg_hr_bpm']} bpm\") | |\n| if w.get(\"notes\"): | |\n| parts.append(f\"— {w['notes']}\") | |\n| lines.append(\" | \".join(parts)) | |\n| return \"\\n\".join(lines) | |\n| @tool | |\n| def get_training_stats() -> str: | |\n| \"\"\"Get summary training statistics across all logged workouts.\"\"\" | |\n| workouts = _load_workouts() | |\n| if not workouts: | |\n| return \"No workouts logged yet.\" | |\n| total_rides = len(workouts) | |\n| total_min = sum(w[\"duration_min\"] for w in workouts) | |\n| total_km = sum(w.get(\"distance_km\") or 0 for w in workouts) | |\n| power_entries = [w[\"avg_power_w\"] for w in workouts if w.get(\"avg_power_w\")] | |\n| avg_power = sum(power_entries) / len(power_entries) if power_entries else None | |\n| by_type: dict[str, int] = {} | |\n| for w in workouts: | |\n| by_type[w[\"type\"]] = by_type.get(w[\"type\"], 0) + 1 | |\n| lines = [ | |\n| f\"Total workouts: {total_rides}\", | |\n| f\"Total time: {total_min // 60}h {total_min % 60}m\", | |\n| f\"Total distance: {total_km:.1f} km\", | |\n| ] | |\n| if avg_power: | |\n| lines.append(f\"Average power: {avg_power:.0f}W\") | |\n| lines.append(\"By type: \" + \", \".join(f\"{k} ({v})\" for k, v in by_type.items())) | |\n| return \"\\n\".join(lines) | |\n| SYSTEM_PROMPT = \"\"\"You are a knowledgeable and encouraging cycling coach assistant. | |\n| Help the user log workouts, review their training history, and provide training advice. | |\n| When giving recommendations, consider periodization, recovery, and progressive overload. | |\n| Keep responses concise and practical. Today's date is {today}.\"\"\" | |\n| def main() -> None: | |\n| api_base = os.environ.get(\"DATAROBOT_ENDPOINT\", \"https://app.datarobot.com/api/v2\") | |\n| api_key = os.environ.get(\"DATAROBOT_API_TOKEN\", \"\") | |\n| llm = ChatLiteLLM( | |\n| model=\"datarobot/bedrock/anthropic.claude-opus-4-8\", | |\n| api_base=api_base, | |\n| api_key=api_key, | |\n| ) | |\n| tools = [log_workout, get_recent_workouts, get_training_stats] | |\n| system = SYSTEM_PROMPT.format(today=date.today().isoformat()) | |\n| agent = create_react_agent(llm, tools, prompt=system) | |\n| print(\"Bike Training Assistant (type 'quit' to exit)\\n\") | |\n| messages = [] | |\n| while True: | |\n| try: | |\n| user_input = input(\"You: \").strip() | |\n| except (EOFError, KeyboardInterrupt): | |\n| print(\"\\nRide on!\") | |\n| break | |\n| if not user_input: | |\n| continue | |\n| if user_input.lower() in (\"quit\", \"exit\", \"q\"): | |\n| print(\"Ride on!\") | |\n| break | |\n| messages.append(HumanMessage(content=user_input)) | |\n| result = agent.invoke({\"messages\": messages}) | |\n| messages = result[\"messages\"] | |\n| reply = messages[-1].content | |\n| print(f\"\\nCoach: {reply}\\n\") | |\n| if __name__ == \"__main__\": | |\n| main() |", "url": "https://wpnews.pro/news/simple-langgraph-bike-training-agent", "canonical_source": "https://gist.github.com/carsongee/afe045b75f9ce694a605d14f69c1cfe2", "published_at": "2026-06-12 23:38:10+00:00", "updated_at": "2026-06-14 21:41:30.944843+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools"], "entities": ["LangGraph", "LangChain", "LiteLLM"], "alternates": {"html": "https://wpnews.pro/news/simple-langgraph-bike-training-agent", "markdown": "https://wpnews.pro/news/simple-langgraph-bike-training-agent.md", "text": "https://wpnews.pro/news/simple-langgraph-bike-training-agent.txt", "jsonld": "https://wpnews.pro/news/simple-langgraph-bike-training-agent.jsonld"}}