GARMIN & AI – Raccogliere i dati con Python

Sottotitolo: Come trasformare i dati raccolti quotidianamente da uno smartwatch in una base informativa strutturata, pronta per essere interrogata da un motore di intelligenza artificiale privato.

Introduzione: il valore dei dati che produciamo ogni giorno

Ogni giorno uno smartwatch raccoglie una quantità sorprendente di informazioni.

Frequenza cardiaca, passi, attività sportive, qualità del sonno, variabilità della frequenza cardiaca (HRV), stress e recupero sono soltanto alcuni dei parametri disponibili.

Nel mio caso utilizzo un Garmin Fenix per monitorare soprattutto gli allenamenti di nuoto, sia in piscina sia in acque libere.

Garmin Connect permette di visualizzare questi dati attraverso grafici e report, ma volevo realizzare qualcosa di diverso.

Volevo che un’intelligenza artificiale potesse leggere i miei dati Garmin, comprenderne l’evoluzione nel tempo e aiutarmi a interpretare allenamenti e recupero.

Per esempio:

  • Come è cambiata la mia frequenza cardiaca durante gli ultimi allenamenti?
  • Sto migliorando l’efficienza della nuotata?
  • Esiste una relazione tra qualità del sonno e prestazioni sportive?
  • Come varia la mia HRV nei periodi di maggiore carico?
  • Quanto recupero dopo un allenamento particolarmente impegnativo?

Per rispondere a queste domande, il primo problema da risolvere non era l’intelligenza artificiale; era l’accesso ai dati.

L’architettura del progetto

Ho deciso di costruire una pipeline completamente ospitata nel mio homelab.

L’architettura complessiva prevede quattro componenti principali:

  1. Garmin Connect,raccoglie e conserva le info provenienti dallo smartwatch.
  2. Garmin Collector, un’applicazione Python che interroga le API e scarica i dati.
  3. Uno storage persistente, nel quale vengono salvati i dati in formato JSON.
  4. Un server MCP, che permette al motore AI di accedere alle informazioni attraverso strumenti dedicati.

Il flusso logico è:

Garmin Fenix
     |
     v
Garmin Connect
     |
     | HTTPS / API
     v
Garmin Collector (Python)
     |
     | Normalizzazione
     v
File JSON strutturati
     |
     v
Storage persistente
     |
     v
Garmin MCP Server
     |
     v
OpenWebUI + Motore AI

In questo primo articolo ci concentreremo esclusivamente sulla parte iniziale: acquisizione, autenticazione, organizzazione e normalizzazione dei dati Garmin.

L’infrastruttura Kubernetes e l’integrazione MCP saranno affrontate nei prossimi articoli.

La prima difficoltà: accedere ai dati Garmin

A differenza di altri servizi, Garmin Connect non offre agli utenti privati una semplice API pubblica, liberamente utilizzabile per scaricare tutti i propri dati attraverso una chiave personale standard.

Garmin mette a disposizione programmi API ufficiali, come Garmin Health API, destinati principalmente a integrazioni approvate.

Esistono inoltre librerie e integrazioni non ufficiali che utilizzano gli endpoint di Garmin Connect.

Nel mio progetto ho utilizzato un sistema di autenticazione basato su una credenziale personale e sul rilascio di token temporanei.

È importante precisare che questa modalità di autenticazione dipende dal servizio effettivamente utilizzato e non deve essere considerata un’interfaccia pubblica Garmin universalmente disponibile.

L’implementazione concreta impiega due passaggi:

  1. Presentare la credenziale personale a un endpoint di autenticazione.
  2. Utilizzare il token ottenuto per le successive richieste API.

Questo approccio evita di trasmettere la credenziale principale a ogni richiesta.

La gestione delle credenziali

Durante lo sviluppo locale, ho utilizzato un file .env.local:

GARMIN_PERSONAL_API_KEY=<chiave_personale>

Il file deve essere escluso dal versionamento Git.

.env.local
token.json
__pycache__/

Le credenziali non devono essere inserite direttamente nel codice Python, nei manifest pubblici Kubernetes o nei repository GitHub.

Nel deployment definitivo, la credenziale viene invece gestita attraverso un Secret Kubernetes.

Il modulo Python garmin_api.py

Per separare l’autenticazione dalla raccolta dei dati, ho organizzato il codice in due moduli:

  • garmin_api.py: autenticazione, token e richieste HTTP.
  • pull.py: acquisizione e salvataggio dei dati Garmin.

Questa separazione permette di modificare il sistema di autenticazione senza riscrivere tutta la logica di raccolta.

La struttura delle directory

garmin-test/
|
|-- garmin_api.py
|-- pull.py
|-- .env.local
|
`-- data/
    |-- activities.json
    |-- daily.json
    |-- sleep.json
    |-- hrv.json
    |-- weight.json
    |-- fitness_age.json
    |-- hr_zones.json
    |-- meta.json
    `-- token.json

Il file token.json è un file operativo contenente informazioni di autenticazione e deve essere protetto come una credenziale.

Lettura della chiave

Il modulo individua il file .env.local nella directory dello script.

Un esempio semplificato:

import os
from pathlib import Path

HERE = Path(__file__).resolve().parent
ENV_FILE = HERE / ".env.local"

def read_key():
    if not ENV_FILE.exists():
        raise RuntimeError(
            "File .env.local non trovato"
        )

    for line in ENV_FILE.read_text().splitlines():
        if line.startswith("GARMIN_PERSONAL_API_KEY="):
            return line.split("=", 1)[1].strip()

    raise RuntimeError("Chiave Garmin mancante")

Questa funzione illustra la lettura della credenziale. Non costituisce da sola un client Garmin completo.

Nella versione eseguita su Kubernetes, lo script di avvio crea il file .env.local a partire dalla variabile d’ambiente fornita da un Secret.

Token temporanei e rinnovo automatico

Uno degli aspetti più importanti del Collector è la gestione dei token.

Nel progetto il token viene memorizzato localmente, in modo da non dover eseguire una nuova autenticazione per ogni richiesta.

La logica generale è:

def get_token(force=False):
    if not force:
        token = read_cached_token()
        if token_is_valid(token):
            return token

    token = request_new_token()
    save_token(token)
    return token

Questo è pseudocodice illustrativo: le funzioni di cache e autenticazione devono essere implementate secondo il protocollo del servizio utilizzato.

Nel mio caso è stata configurata una durata massima locale del token di 20 ore.

La durata della cache non garantisce che il token rimanga valido per tutto quel periodo. Il server remoto può revocarlo o rifiutarlo prima della scadenza.

Per questo motivo è importante gestire anche le risposte HTTP 401.

Gestione degli errori di autenticazione

Il Collector segue questa logica:

Richiesta API
     |
     v
Token disponibile?
     |
     +-- NO --> Richiedi nuovo token
     |
     v
Esegui richiesta
     |
     +-- HTTP 200 --> Elabora i dati
     |
     +-- HTTP 401 --> Rinnova token
                          |
                          v
                    Riprova una volta
                          |
                          +-- OK
                          |
                          +-- Errore: interrompi

Il nuovo tentativo deve essere limitato: ripetere indefinitamente richieste con credenziali non valide non risolve il problema e può provocare ulteriori blocchi.

Durante il progetto abbiamo incontrato proprio un errore HTTP 401.

La causa è stata individuata confrontando la credenziale locale funzionante con quella configurata nell’ambiente Kubernetes.

Il test di autenticazione locale è riuscito, mentre la credenziale precedentemente presente nel Secret veniva rifiutata.

Questo episodio ha dimostrato l’importanza di separare la diagnosi delle credenziali dalla diagnosi dell’infrastruttura.

Quali dati abbiamo deciso di raccogliere

Una volta risolta l’autenticazione, il passo successivo è stato scegliere quali informazioni acquisire.

Ho organizzato la raccolta in diverse categorie.

Dataset Contenuto principale
activities.json Allenamenti, distanze, durate, frequenza cardiaca, carico
daily.json Passi, frequenza cardiaca, Body Battery e riepiloghi giornalieri
sleep.json Durata del sonno e relative fasi
hrv.json Variabilità della frequenza cardiaca
weight.json Storico delle pesate
fitness_age.json Informazioni sull’età fitness
hr_zones.json Zone di frequenza cardiaca
meta.json Metadati della raccolta

Non tutti i campi sono necessariamente disponibili per ogni attività o per ogni giornata.

La disponibilità dipende dal dispositivo, dalle impostazioni Garmin e dalle informazioni effettivamente presenti nell’account.

Un esempio: le attività di nuoto

Per gli allenamenti di nuoto, i dati possono includere informazioni particolarmente interessanti:

  • Distanza percorsa.
  • Durata totale e tempo effettivo.
  • Frequenza cardiaca media e massima.
  • Velocità media.
  • Cadenza delle bracciate.
  • SWOLF, un indicatore combinato di tempo e numero di bracciate.
  • Distanza media per bracciata.
  • Training Effect aerobico e anaerobico.
  • Tempo trascorso nelle zone cardiache.

Questi dati costituiscono una base utile per confrontare le prestazioni nel tempo.

Tuttavia, è necessario interpretare correttamente il contesto: una sessione in piscina non è direttamente confrontabile con una gara in acque libere, dove condizioni ambientali, navigazione e correnti possono influenzare il risultato.

Normalizzare i dati: dal dato grezzo al JSON

Le risposte API possono contenere strutture complesse, campi opzionali e unità di misura differenti.

Per poter utilizzare questi dati in un sistema AI, è utile definire uno schema stabile.

Consideriamo, per esempio, una possibile rappresentazione normalizzata di un allenamento:

{
  "activity_id": "example-001",
  "date": "2026-11-10",
  "sport": "lap_swimming",
  "distance_m": 2400,
  "duration_s": 2880,
  "average_hr_bpm": 128,
  "average_swolf": 42,
  "source": "garmin"
}

I valori riportati sono illustrativi e non rappresentano una specifica attività realmente acquisita.

La normalizzazione permette di:

  • Utilizzare nomi di campo coerenti.
  • Esplicitare le unità di misura.
  • Gestire i valori mancanti.
  • Collegare i dati tramite identificatori e date.
  • Ridurre la complessità delle successive interrogazioni.

È importante conservare anche i dati originali, oppure almeno i campi necessari a ricostruire il significato delle misure.

Organizzare i dati per data

Per i riepiloghi giornalieri, una struttura indicizzata per data risulta particolarmente pratica.

{
  "2026-11-09": {
    "steps": 8500,
    "resting_hr": 54
  },
  "2026-11-10": {
    "steps": 10200,
    "resting_hr": 52
  }
}

Anche questi sono dati di esempio.

Una struttura del genere rende semplice selezionare intervalli temporali e costruire confronti.

Per un’analisi più avanzata, potremo successivamente trasferire i dati in un database relazionale o in un sistema dedicato alle serie temporali.

Per la prima versione del progetto, però, i file JSON sono stati sufficienti.

Il Collector: automatizzare la raccolta

Il secondo script, pull.py, si occupa di orchestrare la raccolta.

Il suo compito è:

  1. Inizializzare il client API.
  2. Richiedere i diversi dataset.
  3. Gestire gli errori.
  4. Salvare i risultati nei file JSON.
  5. Terminare con un codice di uscita che permetta di identificare eventuali fallimenti.

Il principio generale è semplice:

from pathlib import Path
import json

DATA_DIR = Path(__file__).resolve().parent / "data"
DATA_DIR.mkdir(exist_ok=True)

def save_json(filename, payload):
    destination = DATA_DIR / filename

    temporary = destination.with_suffix(".tmp")

    temporary.write_text(
        json.dumps(
            payload,
            ensure_ascii=False,
            indent=2
        ),
        encoding="utf-8"
    )

    temporary.replace(destination)

Questo esempio mostra il salvataggio tramite file temporaneo e sostituzione atomica, quando supportata dal filesystem.

Il codice completo di pull.py deve naturalmente includere le chiamate effettive alle API, la gestione dei dati mancanti e le regole di aggiornamento dei dataset.

La scrittura atomica riduce il rischio di lasciare file JSON parziali in caso di interruzione, ma non sostituisce backup e controlli di integrità.

Validare i dati raccolti

Scaricare un file non significa necessariamente aver acquisito informazioni corrette.

Per questo ho introdotto verifiche sui dataset.

Per esempio:

import json
from pathlib import Path

data_dir = Path("data")

for name in [
    "activities.json",
    "daily.json",
    "sleep.json",
    "hrv.json"
]:
    path = data_dir / name

    if not path.exists():
        print(f"{name}: MANCANTE")
        continue

    with path.open(encoding="utf-8") as f:
        payload = json.load(f)

    print(
        f"{name}: "
        f"{len(payload)} elementi"
    )

Nel nostro progetto abbiamo verificato la presenza di 366 chiavi per i dataset giornalieri relativi a riepiloghi, sonno e HRV durante una delle prime raccolte.

È stata una verifica utile, ma il solo numero di record non basta.

Un Collector affidabile dovrebbe controllare anche:

  • Data più recente disponibile.
  • Presenza di identificatori duplicati.
  • Coerenza delle unità di misura.
  • Integrità sintattica dei JSON.
  • Completezza della finestra temporale richiesta.

La lezione più importante: distinguere raccolta e consultazione

Una delle decisioni architetturali fondamentali è stata separare il componente che raccoglie i dati da quello che li espone all’intelligenza artificiale.

Il Garmin Collector comunica con il servizio remoto.

Il Garmin MCP Server, invece, legge i dati già presenti nello storage.

Questo significa che il motore AI non deve necessariamente autenticarsi presso Garmin ogni volta che riceve una domanda.

Il vantaggio è duplice.

Da un lato, riduciamo la dipendenza dalle API esterne. Dall’altro, manteniamo un archivio locale che può essere consultato anche quando il servizio remoto non è temporaneamente disponibile.

C’è però una conseguenza importante: se il Collector smette di funzionare, il server MCP può continuare a rispondere utilizzando dati non aggiornati.

È esattamente il problema che abbiamo incontrato durante una verifica dell’infrastruttura.

Il server MCP rispondeva correttamente, ma l’ultima attività disponibile risaliva al 1° novembre.

Dopo aver ripristinato l’autenticazione del Collector, una nuova raccolta ha permesso al motore AI di leggere anche l’attività del 10 novembre.

Questo ci ha insegnato che la disponibilità del servizio e la freschezza dei dati sono due indicatori distinti.

In futuro sarà utile esporre anche la data dell’ultima raccolta riuscita, così che l’assistente AI possa dichiarare esplicitamente quanto sono aggiornate le informazioni utilizzate.

Sicurezza e protezione dei dati personali

I dati Garmin comprendono informazioni potenzialmente sensibili, soprattutto quando riguardano salute, sonno e parametri fisiologici.

Per questo motivo il progetto segue alcuni principi:

  • Le credenziali non vengono inserite nel codice.
  • I token sono conservati in file protetti.
  • I dati vengono archiviati su infrastruttura privata.
  • L’accesso agli strumenti MCP viene limitato ai componenti autorizzati.
  • Le informazioni personali non vengono pubblicate nei repository.
  • Le copie di backup devono essere protette quanto i dati originali.

È inoltre importante ricordare che le integrazioni non ufficiali possono cambiare comportamento o smettere di funzionare in seguito a modifiche del servizio remoto.

Un sistema di produzione dovrebbe prevedere monitoraggio, gestione delle credenziali e procedure di recupero.

Conclusioni

Il primo obiettivo del progetto è stato raggiunto: trasformare i dati raccolti da Garmin in informazioni strutturate e disponibili localmente.

Abbiamo costruito una pipeline capace di:

  • Autenticarsi presso il servizio di accesso ai dati Garmin.
  • Gestire token temporanei.
  • Scaricare attività e parametri fisiologici.
  • Organizzare i dati in file JSON.
  • Conservare uno storico utilizzabile da applicazioni successive.
  • Separare la raccolta dalla consultazione tramite intelligenza artificiale.

Il risultato non è ancora un coach AI.

È qualcosa di più fondamentale: una base dati personale, strutturata e interrogabile, sulla quale costruire un sistema intelligente.

Nel prossimo articolo vedremo come portare questa pipeline all’interno di un cluster Kubernetes RKE2, utilizzando CronJob per la raccolta automatica, Secret per le credenziali e volumi persistenti Synology iSCSI per la conservazione dei dati.

Affronteremo anche problemi reali incontrati durante l’implementazione, come i conflitti Multi-Attach e i Job bloccati.

Garmin + Claude: una dashboard di allenamento tutta mia

In un pomeriggio domenicale, il geek in me ha preso il sopravvento e ha deciso di colleare il mio account Garmin a Claude per creare una dashboard di allenamento costruita sulle mie domande, non su quelle di un’app e …. senza scrivere una riga di codice!!!

Nuoto in piscina e in acque libere, cammino, faccio yoga !

Garmin Connect registra tutto, ma ogni schermata risponde a una domanda generica.

Io ne ho di molto precise:  sto diventando più veloce in acqua? Sto recuperando abbastanza? Il peso va nella direzione giusta?

Volevo una pagina sola che rispondesse a queste domande, con i miei dati, e che restasse sul mio computer.

La chiave ufficiale di Garmin

Garmin offre una Personal API: una chiave personale che dà accesso ai propri dati, in sola lettura.

Niente librerie non ufficiali, niente password salvate in giro.

La chiave si crea in Garmin Connect, nella pagina Personal API Key, con una scadenza a scelta.

Viene mostrata una sola volta e ne può esistere una sola attiva.

Con la chiave si ottiene un token che dura 24 ore, e con il token si leggono attività, riepiloghi giornalieri, sonno, HRV, peso, età fitness e zone cardio.

Sola lettura significa che nessuno script può modificare o cancellare nulla nel mio account.

Un passo alla volta, senza scrivere codice

Ho usato Claude in modalità Cowork, collegato al mio Mac.

Prima ho scritto le istruzioni una volta sola e le ho salvate come skill, così posso riusarle.

Poi Claude mi ha guidato in sette passi, fermandosi ogni volta che serviva qualcosa da me.

  1. Progetto: Claude ha creato la cartella, verificato Python e preparato i file di configurazione.
  2. Chiave: Ho creato la chiave su Garmin Connect seguendo le istruzioni, per poi  copiarla nel file giusto.
  3. Test: un comando nel Terminale ha mostrato le mie ultime cinque attività. Connessione funzionante 🙂
  4. Domande: obiettivo, sport, stile grafico, unità di misura, numeri che guardo di più.
  5. Download: 12 mesi di storia in circa un minuto.
  6. Dashboard: un unico file HTML che si apre nel browser.
  7. Aggiornamento: un solo comando scarica le novità e ricostruisce tutto.

Il mio lavoro è stato  copiare una chiave e incollare due righe nel terminale, il  codice l’ha scritto e controllato Claude.

Le regole d’oro: chiave e dati restano a casa

Sono dati di salute, quindi ho fissato poche regole chiare fin dall’inizio.

  • Sola lettura: nessuna chiamata che modifichi il mio account.
  • La chiave non passa dalla chat: la incollo io in un file sul Mac. Claude controlla solo che ci sia, senza mai leggerla o mostrarla.
  • Tutto in locale: chiave, dati scaricati e dashboard restano sul mio computer, esclusi da qualsiasi caricamento online.
  • Niente cloud aziendale: avevo chiesto di mettere il progetto in Documenti, ma Claude mi ha fatto notare che sul mio Mac quella cartella è sincronizzata con OneDrive. Abbiamo scelto una cartella solo locale.

Anche il backup che tengo su Dropbox contiene solo il codice, mai la chiave né i dati.

Cosa c’è nella dashboard

Ogni grafico risponde a una domanda che mi faccio davvero. Lo stile è pulito e chiaro, come un diario di allenamento, e funziona anche sul telefono. Passando sopra un grafico vedo i valori esatti del giorno o della settimana.

Sezione La domanda Cosa mostra
Verso l’obiettivo Sto andando nella direzione giusta? Peso, passo in piscina, km in acque libere, volume di nuoto
Salute Come sto oggi rispetto al solito? HRV, FC a riposo, sonno, carico, Body Battery, età fitness
Questa settimana Mi sto allenando come al solito? Km, ore e sessioni per sport, ultime 16 settimane
Tendenze Cosa sta cambiando nei mesi? HRV con la fascia personale, FC a riposo, fasi del sonno, carico, stress
Nuoto Sto diventando più veloce? Passo per 100 m di ogni nuotata, km al mese
Record Qual è il mio meglio? Nuotate più lunghe, passo migliore, giornate più impegnative

Cosa mi hanno detto i miei dati

In un anno: 700 attività, 366 notti di sonno e HRV, 19 uscite in acque libere. Vederli tutti insieme ha cambiato la mia lettura.

  • Recupero: in questo periodo l’HRV notturna è sotto la mia fascia abituale e la FC a riposo della settimana è sopra la media del mese. Due segnali che, insieme, chiedono un po’ più di riposo.
  • Velocità in acqua: il passo medio in piscina è intorno a 1:58 ogni 100 m, con un miglior risultato di 1:35 su almeno un chilometro. Ora posso seguirlo nuotata dopo nuotata.
  • Acque libere: la nuotata più lunga è stata di 3,64 km. È il punto di partenza per le gare.
  • Peso: solo 7 pesate in un anno sono troppo poche per seguire un obiettivo. La prima cosa da cambiare non è l’allenamento, ma l’abitudine di salire sulla bilancia.

Aggiornarla e portarla con sé

Dopo un allenamento basta un comando: scarica solo le novità, ricostruisce la pagina e la apre. La chiave dura un anno, poi se ne crea una nuova in due minuti.

Se cambio computer, copio la cartella del progetto, installo Python e rilancio lo stesso comando. Su Windows c’è un file equivalente con doppio clic.

In conclusione

Non serve essere programmatori per avere i propri dati in una forma utile.

Serve sapere quali domande farsi e avere un assistente che traduca quelle domande in codice, rispettando le regole che decidiamo noi.

La mia prossima domanda per la dashboard è già pronta: quanti giorni mancano alla prossima gara in acque libere?

Dal diario di allenamento a un coach AI personale

Sottotitolo: Come trasformare anni di allenamenti, dati e passione per il nuoto in un agente AI capace di conoscere la mia storia sportiva

I primi due giorni di ferie, normalmente, dovrebbero servire a staccare.

Nel mio caso è successo più o meno il contrario.

Da buon nerd, appassionato di tecnologia e grande cultore del nuoto in acque libere, mi sono posto una domanda che, come spesso accade, sembrava innocua:

“E se mi costruissi un agente AI che conosce davvero il mio modo di allenarmi?”

Non un chatbot al quale chiedere genericamente “fammi un allenamento di nuoto da 3.000 metri”. Quello è relativamente semplice.

Volevo qualcosa di diverso.

  1. Un agente che conoscesse anni di allenamenti, sapesse come sono strutturate le mie Sedute A e B, riconoscesse A1, A2, B1, B2, C1, pull, pinne, boccaglio, remate e lavori di sensibilità.
  2. Un sistema che conoscesse i miei riferimenti prestativi, sapesse distinguere ciò che avrei dovuto fare da ciò che ho realmente fatto e, soprattutto, utilizzasse tutto questo contesto prima di suggerirmi cosa fare dopo.

Così sono iniziati i miei primi due giorni di ferie.

E naturalmente sono finiti con Final Surge, Garmin, Strava, API, OAuth, Knowledge Base e un agente AI.

Forse la definizione di ferie per un nerd è leggermente diversa.

Il problema non sono i dati. È riuscire a usarli !

Come molti sportivi che utilizzano tecnologia durante gli allenamenti, negli anni ho accumulato una quantità notevole di informazioni.

  • Gli allenamenti programmati sono in Final Surge.
  • L’orologio Garmin registra ciò che faccio realmente.
  • Garmin Connect raccoglie le attività. Strava ne riceve una copia.

Il risultato è che possiedo moltissimi dati, ma possedere dati non significa necessariamente possedere conoscenza.

Se voglio sapere cosa ho fatto ieri, è semplice.

Se invece voglio rispondere a domande come:

  • Come sono cambiate le mie Sedute A negli ultimi sei mesi?
  • Quali esercizi tecnici ho utilizzato maggiormente?
  • Quanto lavoro A2 sto facendo?
  • L’allenamento che dovrei fare domani è coerente con le ultime settimane?

la situazione cambia!

I dati ci sono, ma sono distribuiti in sistemi diversi e soprattutto manca qualcosa che li metta in relazione.

Ed è qui che l’AI diventa interessante.

Primo passo: Dare una memoria al coach

La prima operazione è stata recuperare lo storico da Final Surge.

L’obiettivo non era semplicemente esportare dei PDF e caricarli dentro un GPT. Volevo trasformare quello storico in una Knowledge Base utilizzabile dall’agente.

Ho quindi estratto esclusivamente gli allenamenti di nuoto, identificati dalle mie classiche voci Seduta A, Seduta B, ecc., eliminando tutto ciò che non era pertinente.

Il risultato è stato un archivio strutturato di 221 allenamenti, dal quale l’AI può recuperare una determinata seduta, confrontare periodi differenti e riconoscere progressioni ed elementi ricorrenti.

Questo cambia radicalmente il tipo di domanda che posso fare.

Non più: “Creami un allenamento da 3.000 metri.”

Ma: “Analizza le ultime tre Sedute A e proponimi la successiva, mantenendo una progressione coerente.”

Sono due richieste apparentemente simili, ma concettualmente molto diverse.

Nel primo caso l’AI genera, nel secondo deve prima recuperare, comprendere e contestualizzare.

Ma, una Knowledge Base non basta

A quel punto è emerso un secondo problema.

L’agente conosceva gli allenamenti, ma non conosceva abbastanza bene l’atleta.

Ho quindi creato un secondo elemento: il Profilo Atleta.

Qui sono raccolti i riferimenti utili per interpretare lo storico:

Età, disciplina prevalente, frequenza degli allenamenti, piscina e acque libere, obiettivi e alcuni riferimenti prestativi.

Per esempio:

200 m → 3:45
400 m → 7:42
1.000 m → 19:22
CSS storico → circa 1:58/100 m

Con una regola fondamentale: un dato storico non deve essere considerato automaticamente un dato attuale.

Se domani effettuo un nuovo test, il sistema deve utilizzare il nuovo valore senza perdere quello precedente, perché proprio il confronto nel tempo permette di capire l’evoluzione.

A questo punto iniziava a delinearsi una struttura interessante:

  • Knowledge → cosa ho fatto nel tempo
  • Profilo → chi sono e quali sono i miei riferimenti
  • Conversazione → qual è la mia situazione oggi

Poi arriva il problema dei 3.000 metri.

Chi prepara allenamenti di nuoto probabilmente sorriderà.

Chiedere a un’AI: “Preparami una seduta esattamente da 3.000 metri”, può produrre una bellissima scheda che termina con:

TOTALE: 3.000 m

Peccato che, facendo i conti, magari siano 2.850.

Un piccolo dettaglio: quando sei in acqua e scopri che il totale scritto in fondo alla scheda è più ottimista della matematica.

Per questo nelle Instructions di AI-nuoto ho inserito una regola quasi ossessiva:

il totale non si dichiara, si calcola.

Ogni blocco deve essere calcolato, le moltiplicazioni delle serie devono essere considerate e alla fine tutto deve essere sommato nuovamente.

Durante il collaudo ho quindi chiesto all’agente di analizzare le ultime tre Sedute A e crearne una nuova esattamente da 3.000 metri.

Il risultato finale è stato:

Riscaldamento: 600 m
Esercizi: 400 m
Attivazione: 400 m
Lavoro: 1.200 m
Defaticamento: 400 m

Totale:

3.000 m.

Questa volta anche secondo la matematica.

Può sembrare una banalità, ma è un esempio molto concreto di un principio importante quando costruiamo sistemi AI: non basta fornire informazioni al modello. Dobbiamo anche definire le regole con cui deve utilizzarle.

Ma….  mancava ancora metà della storia.

Nel prossimo articolo vedremo cosa serve

Generare allenamenti di nuoto con un LLM locale

Generare allenamenti di nuoto con un LLM locale: dalla knowledge base alla pianificazione settimanale

Nel precedente articolo abbiamo visto come costruire un sistema RAG (Retrieval Augmented Generation) nel nostro laboratorio Kubernetes utilizzando:

  • Ollama come motore LLM locale
  • Qdrant come database vettoriale
  • OpenWebUI come interfaccia per l’interazione con il modello

In quella fase l’obiettivo era permettere al modello di rispondere a domande utilizzando una knowledge base specifica.

Il passo successivo è stato molto più interessante: utilizzare lo stesso sistema per generare allenamenti di nuoto realistici, basati su allenamenti realmente utilizzati durante la stagione.

Dalla conoscenza alla generazione di allenamenti

Un modello linguistico può generare contenuti plausibili, ma senza un contesto specifico tende a produrre risultati generici.

Per ottenere allenamenti realistici abbiamo costruito una knowledge base composta da sedute reali di allenamento, strutturate secondo il modello CSS (Critical Swim Speed).

Ogni allenamento è stato inserito nella libreria come documento indipendente, ad esempio:

  • workout_week24_sessionA.txt
  • workout_week24_sessionB.txt
  • workout_week24_sessionC.txt

Ogni file segue una struttura coerente:

  • WARMUP
  • SENSITIVITY
  • TECHNIQUE
  • ACTIVATION
  • MAIN SET
  • COOLDOWN

Questo approccio permette al sistema RAG di recuperare esempi reali e fornire al modello pattern di allenamento realistici.

Il ruolo della knowledge base

La knowledge base contiene tre tipi di informazioni:

1. Regole del metodo di allenamento

Un documento descrive il metodo utilizzato per costruire le sedute.

Ad esempio: coach_training_rules.txt

In questo file sono definite:

  • le zone di allenamento (A1, A2, B1, B2, C1, C2)
  • la struttura delle sedute
  • il ruolo dei drill tecnici
  • l’utilizzo degli attrezzi (pinne, pull buoy, palette, snorkel)

Questo documento permette al modello di comprendere la logica dell’allenatore, non solo copiare esempi.

2. Allenamenti reali

La libreria contiene numerose sedute distribuite su diverse settimane.

Ad esempio: week21, week22, week23, week24, week25, week26, week27, week28

Ogni settimana include più sessioni di allenamento.

Questo consente al modello di osservare:

  • progressioni di carico
  • variazioni di intensità
  • alternanza tra tecnica e lavoro aerobico
  • uso delle diverse zone di allenamento

3. Regole di generazione

Un ulteriore documento definisce alcune regole utili per generare allenamenti coerenti:

  • distanza totale tipica (2000–2800 m)
  • utilizzo delle zone CSS
  • presenza obbligatoria delle sezioni della seduta
  • uso di set concreti con distanze e recuperi

4. Generazione di nuovi allenamenti

Una volta popolata la knowledge base, il sistema è in grado di generare nuovi allenamenti utilizzando i pattern presenti negli esempi.

Un esempio di richiesta può essere: (prompt)

Generate a 2500m swim workout using CSS zones
similar to the sessions in the knowledge base.

Il sistema RAG recupera i documenti più rilevanti dalla libreria e li utilizza come contesto per il modello.

Il risultato è una seduta strutturata, ad esempio:

—
WARMUP
200 freestyle
100 pullSENSITIVITY
2x(30″ sculling + 50 freestyle)ACTIVATION
4×50 (1 A2 + 1 B1)MAIN SET 2x
150 A2
150 B1
2×100 A1 pullCOOLDOWN
200 easy swim

Il vantaggio principale è che il modello non inventa un allenamento casuale, ma utilizza pattern già presenti nella libreria.

Verso la pianificazione settimanale

Una volta che il sistema è in grado di generare singole sedute, il passo successivo è generare una settimana completa di allenamento.

Ad esempio:

Generate week 29 swim training
3 sessions
based on weeks 21–28 patterns

In questo caso il modello utilizza le settimane precedenti per creare una nuova settimana coerente con il metodo di allenamento.

Questo permette di utilizzare il sistema come assistente per la pianificazione degli allenamenti.

Nel prossimo articolo vedremo i passaggi all’interno di webui per realizzare il progetto swimming-training

Configurare OpenWebUI per generare allenamenti con RAG

Nei precedenti articoli abbiamo visto come funziona il Retrieval Augmented Generation (RAG) nel nostro laboratorio AI basato su Kubernetes.

In questo articolo vedremo come configurare OpenWebUI per permettere al modello di generare allenamenti di nuoto realistici utilizzando una knowledge base costruita con allenamenti reali.

Obiettivo: trasformare OpenWebUI in un assistente di coaching basato su AI.

Architettura del sistema

Il flusso di lavoro creato è il seguente:

Utente
↓
OpenWebUI
↓
RAG Engine
↓
Qdrant (knowledge base)
↓
Ollama (LLM locale)

Quando l’utente chiede di generare un allenamento:

  1. OpenWebUI interroga la knowledge base
  2. Qdrant restituisce i documenti più simili
  3. Il modello LLM utilizza quei documenti come contesto
  4. Viene generata una nuova seduta di allenamento

Preparazione della Knowledge Base

Il primo passo consiste nel caricare nella piattaforma gli allenamenti reali; nel nostro caso la libreria contiene diverse sedute organizzate per settimana:

week21_sessionA, week22_sessionB, week23_sessionC, week24_sessionA, week25_sessionB, week26_sessionA, week27_sessionA

Ogni documento contiene una seduta completa con la seguente struttura:

  • WARMUP
  • SENSITIVITY
  • TECHNIQUE
  • ACTIVATION
  • MAIN SET
  • COOLDOWN

Questi documenti costituiscono il corpus di conoscenza che il sistema utilizzerà per generare nuovi allenamenti.

Caricare i documenti nella sezione Knowledge

Dall’interfaccia di OpenWebUI:

  1. Aprire il menu laterale
  2. Selezionare Workspace
  3. Entrare nella sezione Knowledge
  4. Creare una nuova knowledge base

Nel mio lab dove le sessioni di allenamento sono specifiche per il nuoto la libreria è stata nominata swim_training_library.

Una volta creata la knowledge base è possibile caricare i documenti:

  • coach_training_rules.txt
  • week21_sessionA.txt
  • week22_sessionB.txt
  • week23_sessionC.txt
    …

Durante l’upload OpenWebUI esegue automaticamente:

  • creazione degli embeddings
  • indicizzazione nel vector database Qdrant

In questo modo i documenti diventano interrogabili tramite ricerca semantica.

Creare un Prompt Skill

Una volta caricata la libreria è utile creare uno skill dedicato alla generazione degli allenamenti.

Gli Skills permettono di definire prompt riutilizzabili che guidano il comportamento del modello.

Nel nostro caso creiamo uno skill chiamato:

swim_workout_generator

All’interno dello skill possiamo inserire un prompt come il seguente.

“Generate a swim workout using the CSS methodology.

Rules:
– use only patterns from the knowledge base
– use zones A1 A2 B1 B2 C1 C2
– include sections:
WARMUP
SENSITIVITY
TECHNIQUE
ACTIVATION
MAIN SET
COOLDOWN
– total distance between 2000m and 2800m
– rest intervals between 10 and 30 seconds
– avoid generic descriptions”

Questo prompt permette di stabilizzare il comportamento del modello.

Collegare la Knowledge allo Skill

Durante la configurazione dello skill è possibile specificare la knowledge base da utilizzare.

Nel nostro caso selezioniamo:

swim_training_library

In questo modo ogni volta che lo skill viene utilizzato:

  • OpenWebUI esegue una ricerca nella knowledge base
  • i documenti più rilevanti vengono passati al modello

Questo è il cuore del RAG workflow.

Utilizzo dei Tools

OpenWebUI permette anche di aggiungere Tools.

I tools sono funzioni che il modello può richiamare per eseguire operazioni esterne.

Nel laboratorio verranno utilizzati per:

  • generare allenamenti
  • convertire gli allenamenti in formato Garmin
  • esportare i workout

Ad esempio nel prossimo step verrà aggiunto un tool Python per generare file di tipo .tcx compatibili con Garmin Connect.

Creare una Workspace dedicata

Per organizzare il lavoro è utile creare un workspac dedicato.

Ad esempio: AI Swim Coach

All’interno della workspace possiamo raccogliere:

  • la knowledge base degli allenamenti
  • gli skills di generazione
  • i tools di esportazione

Così facendo possiamo trasformare OpenWebUI in un vero ambiente di coaching AI.

Test del sistema

Una volta completata la configurazione possiamo testare il sistema.

Ad esempio con una richiesta come:

“Generate a 2500m swim workout using the CSS training methodology”.

Il sistema recupererà i documenti più rilevanti dalla libreria e produrrà un nuovo allenamento coerente con gli esempi reali.

NB: Il file coach_training_rules.txt è quello che spiega al modello come ragiona l’allenatore. Non contiene allenamenti, ma le regole del metodo

  • come sono strutturate le sedute
  • cosa significano le zone
  • quali esercizi sono ammessi
  • che stile deve avere l’allenamento