Dwie agentowe funkcje dla Wattlog.pro: audytor i trener po treningu¶
Jak grafy stanów LangGraph zamieniają surowe dane z treningu w decyzje, nie tylko wykresy — i jak wpięłyby się w istniejący backend Wattlog.pro oparty na FastAPI/WebSocket.
Wattlog już liczy liczby, na których zależy poważnym kolarzom: TSS na sesję, trend CTL/ATL/TSB, gotowość na podstawie HRV. Czego jeszcze nie robi, to działanie na podstawie tych danych. Dashboard mówi, że TSB wynosi -34. Nie mówi, co z tym zrobić, i nie dotyka jutrzejszego planu.
To problem dla agenta, nie dla dashboardu — workflow, który czyta stan, stosuje reguły domenowe i podejmuje warunkową akcję. Dwie konkretne funkcje dobrze pasują do tego kształtu. Obie to grafy LangGraph siedzące za istniejącą warstwą analityczną Wattlog, a nie przepisanie jej od zera.
Przypadek 1: Automatyczny audytor obciążenia treningowego¶
Zamiast zawodnika zgadującego, czy nie trenuje za dużo, stanowy graf audytuje trend i ingeruje bezpośrednio w plan.
[Pobierz dane zawodnika] → [Analiza zmęczenia: TSS/CTL/ATL] → [Sprawdź kolejną jazdę] → [Warunek: dostosuj albo zatwierdź]
Węzeł 1 — Pobierz dane zawodnika. Ściąga ostatnie 14 dni z magazynu sesji Wattlog: czasy trwania, średnią moc, TSS per sesja. Wattlog już to trzyma w SQLite na potrzeby wykresu PMC, więc ten węzeł to odczyt, nie nowy pipeline.
Węzeł 2 — Analiza zmęczenia. Liczy standardową matematykę PMC (CTL = 42-dniowa wykładniczo ważona TSS, ATL = 7-dniowa, TSB = CTL − ATL). Jeśli TSB spada poniżej progu (np. -30), stan dostaje flagę fatigued i graf idzie warunkową krawędzią zamiast domyślnym przejściem.
Węzeł 3 — Sprawdź kolejną jazdę. Czyta zaplanowany trening z biblioteki planów — intensywność, strefy docelowe, czas trwania.
Węzeł 4 — Warunek: dostosuj albo zatwierdź.
- Świeży (TSB powyżej progu): zatwierdza zaplanowaną sesję interwałową bez zmian.
- Zmęczony: przechwytuje plan, generuje wyjaśnienie w prostym języku, oparte na własnych liczbach zawodnika ("twój TSB to -34, trzeci dzień z rzędu poniżej -20 — to trend zmęczenia, nie jedna gorsza noc"), i przepisuje jutrzejszą sesję na 45 minut regeneracji.
from langgraph.graph import StateGraph, END
def fetch_athlete_data(state):
state["rides"] = db.get_recent_sessions(state["athlete_id"], days=14)
return state
def analyze_fatigue(state):
ctl, atl = compute_pmc(state["rides"])
state["tsb"] = ctl - atl
state["status"] = "fatigued" if state["tsb"] < -30 else "fresh"
return state
def check_next_ride(state):
state["next_ride"] = db.get_scheduled_workout(state["athlete_id"])
return state
def route(state):
return "adjust" if state["status"] == "fatigued" else "approve"
def adjust_plan(state):
state["revised_ride"] = build_recovery_spin(minutes=45)
state["message"] = coach_llm.explain_adjustment(state["tsb"], state["rides"])
return state
def approve_plan(state):
state["message"] = "TSB w normie — sesja interwałowa zatwierdzona zgodnie z planem."
return state
graph = StateGraph(dict)
graph.add_node("fetch", fetch_athlete_data)
graph.add_node("analyze", analyze_fatigue)
graph.add_node("check_ride", check_next_ride)
graph.add_node("adjust", adjust_plan)
graph.add_node("approve", approve_plan)
graph.set_entry_point("fetch")
graph.add_edge("fetch", "analyze")
graph.add_edge("analyze", "check_ride")
graph.add_conditional_edges("check_ride", route, {"adjust": "adjust", "approve": "approve"})
graph.add_edge("adjust", END)
graph.add_edge("approve", END)
auditor = graph.compile()
Dlaczego graf, a nie skrypt. Sama reguła (TSB < -30 → regeneracja) to jedna linijka Pythona. To, co uzasadnia LangGraph, to wszystko dookoła: trwały stan między węzłami, warunkowa krawędź zamiast zagnieżdżonych ifów zaszytych w jobie schedulera, i checkpointer (PostgresSaver), dzięki któremu przerwany run wznawia się od "sprawdź kolejną jazdę", zamiast pobierać i analizować od nowa. To też czysty szew do dodawania węzłów później — piąty węzeł sprawdzający gotowość HRV obok TSB, bez ruszania pierwszych czterech.
Integracja z Wattlog. Działa jako nocny job na istniejącym backendzie FastAPI — ten sam magazyn sesji SQLite, ten sam kod PMC, którego już używa wykres Fitness Trend. Wynik ląduje jako push notification i karta na dashboardzie obok wykresu PMC, nie osobna powierzchnia. Żadnego nowego pipeline'u danych — graf jest konsumentem danych, które Wattlog już zbiera.
Przypadek 2: Trener podsumowujący po treningu¶
Audytor patrzy do przodu — czy jutro coś zmienić? Trener podsumowujący patrzy wstecz, tuż po zakończeniu sesji, i odpowiada na pytanie, które ma każdy zawodnik po ciężkiej jeździe: jak właściwie poszło, w kontekście wszystkiego, co było wcześniej?
[Koniec sesji] → [Pobierz sesję + kontekst historyczny] → [Porównaj z bazą/celem] → [Wygeneruj podsumowanie] → [Zaktualizuj pamięć zawodnika]
Węzeł 1 — Pobierz sesję + kontekst historyczny. Moc znormalizowana, intensity factor, TSS, strefy HR i przebieg RR dla właśnie zakończonej sesji, plus rolling baseline HRV zawodnika i ostatnie 4 tygodnie PMC — dane, które analityka sesji Wattlog już produkuje.
Węzeł 2 — Porównaj z bazą/celem. Sprawdza sesję względem tego, co było zaplanowane (strefy docelowe, czas trwania) i względem trendu zawodnika (czy to rekord? sygnał dekoupling? sesja zgodna z zadeklarowanym celem, np. "podnieść FTP do sierpnia"?).
Węzeł 3 — Wygeneruj podsumowanie. Wywołanie LLM, ściśle zakotwiczone w liczbach pobranych w węzłach 1–2 (żadnych oderwanych porad), produkuje krótkie podsumowanie w naturalnym języku — bliższe SMS-owi od partnera treningowego niż wygenerowanemu raportowi. Tu liczy się ton: roadmapa Cycling Trainer Hub już zakłada "mówi jak kumpel, który zna się na rzeczy, nie jak sędzia wyników" — i to ograniczenie należy zaszyć w prompcie, nie tylko w redakcji tekstów.
Węzeł 4 — Zaktualizuj pamięć zawodnika. Zapisuje trwałe fakty do magazynu per zawodnik — pamięć semantyczna (cel: "5 km na rowerze poniżej 20 minut", aktualny szacunek FTP), nie historia czatu. Dzięki temu podsumowanie #40 może odnieść się do podsumowania #12 bez wrzucania całego logu rozmów do kontekstu. To też pozwala, żeby Audytor z przypadku 1 i Trener podsumowujący dzielili stan zawodnika, zamiast każdy z osobna go wyliczać.
Integracja z Wattlog. Wyzwalane przez to samo zdarzenie WebSocket, które już strzela, gdy sesja zostaje sfinalizowana i zapisana w bazie analitycznej — graf podsumowania jest subskrybentem tego zdarzenia, nie nową ścieżką wyzwalania. Wynik trafia do istniejącego widoku szczegółów sesji jako notatka trenera nad wykresami. Pamięć żyje w małej dedykowanej tabeli (athlete_id, fact, updated_at), osobno od danych sesji, więc przetrwa niezależnie od pojedynczej jazdy.
Wspólny wzorzec¶
Oba grafy czytają z tego samego źródła prawdy (magazyn sesji Wattlog i obliczenia PMC) i oba kończą, zapisując jedną rzecz z powrotem: decyzję albo notatkę, nie ścianę liczb. To jest realna wartość postawienia LangGraph przed platformą analityczną — zawodnik przestaje interpretować wykres, zaczyna dostawać telefon. Dashboard zostaje źródłem prawdy; grafy to cienka, audytowalna warstwa, która zamienia "oto twój TSB" w "oto co zrobić z powodu twojego TSB", z rozumowaniem możliwym do prześledzenia w każdym węźle.
Następny krok: podpięcie nocnego runa Audytora do istniejącego serwisu powiadomień Wattlog i dodanie UI do tabeli pamięci Trenera podsumowującego, żeby zawodnicy widzieli — i mogli poprawić — to, co trener "zapamiętał" o ich celach.