Skip to content

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.