feat: Training Type Profiles Phase 1.2 - Auto-evaluation (#15)
Automatic evaluation on activity INSERT/UPDATE:
- create_activity(): Evaluate after manual creation
- update_activity(): Re-evaluate after manual update
- import_activity_csv(): Evaluate after CSV import (INSERT + UPDATE)
- bulk_categorize_activities(): Evaluate after bulk training type assignment
All evaluation calls wrapped in try/except to prevent activity operations
from failing if evaluation encounters an error. Only activities with
training_type_id assigned are evaluated.
Phase 1.2 complete ✅
## Next Steps (Phase 2):
Admin-UI for training type profile configuration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1b9cd6d5e6
commit
e11953736d
|
|
@ -16,6 +16,7 @@ from auth import require_auth, check_feature_access, increment_feature_usage
|
||||||
from models import ActivityEntry
|
from models import ActivityEntry
|
||||||
from routers.profiles import get_pid
|
from routers.profiles import get_pid
|
||||||
from feature_logger import log_feature_usage
|
from feature_logger import log_feature_usage
|
||||||
|
from evaluation_helper import evaluate_and_save_activity
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/activity", tags=["activity"])
|
router = APIRouter(prefix="/api/activity", tags=["activity"])
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -64,6 +65,26 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default
|
||||||
d['kcal_active'],d['kcal_resting'],d['hr_avg'],d['hr_max'],d['distance_km'],
|
d['kcal_active'],d['kcal_resting'],d['hr_avg'],d['hr_max'],d['distance_km'],
|
||||||
d['rpe'],d['source'],d['notes']))
|
d['rpe'],d['source'],d['notes']))
|
||||||
|
|
||||||
|
# Phase 1.2: Auto-evaluation after INSERT
|
||||||
|
# Load the activity data to evaluate
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, profile_id, date, training_type_id, duration_min,
|
||||||
|
hr_avg, hr_max, distance_km, kcal_active, kcal_resting,
|
||||||
|
rpe, pace_min_per_km, cadence, elevation_gain
|
||||||
|
FROM activity_log
|
||||||
|
WHERE id = %s
|
||||||
|
""", (eid,))
|
||||||
|
activity_row = cur.fetchone()
|
||||||
|
if activity_row:
|
||||||
|
activity_dict = dict(activity_row)
|
||||||
|
training_type_id = activity_dict.get("training_type_id")
|
||||||
|
if training_type_id:
|
||||||
|
try:
|
||||||
|
evaluate_and_save_activity(cur, eid, activity_dict, training_type_id, pid)
|
||||||
|
logger.info(f"[AUTO-EVAL] Evaluated activity {eid} on INSERT")
|
||||||
|
except Exception as eval_error:
|
||||||
|
logger.error(f"[AUTO-EVAL] Failed to evaluate activity {eid}: {eval_error}")
|
||||||
|
|
||||||
# Phase 2: Increment usage counter (always for new entries)
|
# Phase 2: Increment usage counter (always for new entries)
|
||||||
increment_feature_usage(pid, 'activity_entries')
|
increment_feature_usage(pid, 'activity_entries')
|
||||||
|
|
||||||
|
|
@ -79,6 +100,27 @@ def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Head
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
cur.execute(f"UPDATE activity_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s",
|
cur.execute(f"UPDATE activity_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s",
|
||||||
list(d.values())+[eid,pid])
|
list(d.values())+[eid,pid])
|
||||||
|
|
||||||
|
# Phase 1.2: Auto-evaluation after UPDATE
|
||||||
|
# Load the updated activity data to evaluate
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, profile_id, date, training_type_id, duration_min,
|
||||||
|
hr_avg, hr_max, distance_km, kcal_active, kcal_resting,
|
||||||
|
rpe, pace_min_per_km, cadence, elevation_gain
|
||||||
|
FROM activity_log
|
||||||
|
WHERE id = %s
|
||||||
|
""", (eid,))
|
||||||
|
activity_row = cur.fetchone()
|
||||||
|
if activity_row:
|
||||||
|
activity_dict = dict(activity_row)
|
||||||
|
training_type_id = activity_dict.get("training_type_id")
|
||||||
|
if training_type_id:
|
||||||
|
try:
|
||||||
|
evaluate_and_save_activity(cur, eid, activity_dict, training_type_id, pid)
|
||||||
|
logger.info(f"[AUTO-EVAL] Re-evaluated activity {eid} on UPDATE")
|
||||||
|
except Exception as eval_error:
|
||||||
|
logger.error(f"[AUTO-EVAL] Failed to re-evaluate activity {eid}: {eval_error}")
|
||||||
|
|
||||||
return {"id":eid}
|
return {"id":eid}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -214,6 +256,30 @@ def bulk_categorize_activities(
|
||||||
""", (training_type_id, training_category, training_subcategory, pid, activity_type))
|
""", (training_type_id, training_category, training_subcategory, pid, activity_type))
|
||||||
updated_count = cur.rowcount
|
updated_count = cur.rowcount
|
||||||
|
|
||||||
|
# Phase 1.2: Auto-evaluation after bulk categorization
|
||||||
|
# Load all activities that were just updated and evaluate them
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, profile_id, date, training_type_id, duration_min,
|
||||||
|
hr_avg, hr_max, distance_km, kcal_active, kcal_resting,
|
||||||
|
rpe, pace_min_per_km, cadence, elevation_gain
|
||||||
|
FROM activity_log
|
||||||
|
WHERE profile_id = %s
|
||||||
|
AND activity_type = %s
|
||||||
|
AND training_type_id = %s
|
||||||
|
""", (pid, activity_type, training_type_id))
|
||||||
|
|
||||||
|
activities_to_evaluate = cur.fetchall()
|
||||||
|
evaluated_count = 0
|
||||||
|
for activity_row in activities_to_evaluate:
|
||||||
|
activity_dict = dict(activity_row)
|
||||||
|
try:
|
||||||
|
evaluate_and_save_activity(cur, activity_dict["id"], activity_dict, training_type_id, pid)
|
||||||
|
evaluated_count += 1
|
||||||
|
except Exception as eval_error:
|
||||||
|
logger.warning(f"[AUTO-EVAL] Failed to evaluate bulk-categorized activity {activity_dict['id']}: {eval_error}")
|
||||||
|
|
||||||
|
logger.info(f"[AUTO-EVAL] Evaluated {evaluated_count}/{updated_count} bulk-categorized activities")
|
||||||
|
|
||||||
# Save mapping for future imports (upsert)
|
# Save mapping for future imports (upsert)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO activity_type_mappings (activity_type, training_type_id, profile_id, source, updated_at)
|
INSERT INTO activity_type_mappings (activity_type, training_type_id, profile_id, source, updated_at)
|
||||||
|
|
@ -275,6 +341,7 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Update existing entry (e.g., to add training type mapping)
|
# Update existing entry (e.g., to add training type mapping)
|
||||||
|
existing_id = existing['id']
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE activity_log
|
UPDATE activity_log
|
||||||
SET end_time = %s,
|
SET end_time = %s,
|
||||||
|
|
@ -297,22 +364,73 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
|
||||||
tf(row.get('Max. Herzfrequenz (count/min)','')),
|
tf(row.get('Max. Herzfrequenz (count/min)','')),
|
||||||
tf(row.get('Distanz (km)','')),
|
tf(row.get('Distanz (km)','')),
|
||||||
training_type_id, training_category, training_subcategory,
|
training_type_id, training_category, training_subcategory,
|
||||||
existing['id']
|
existing_id
|
||||||
))
|
))
|
||||||
skipped += 1 # Count as skipped (not newly inserted)
|
skipped += 1 # Count as skipped (not newly inserted)
|
||||||
|
|
||||||
|
# Phase 1.2: Auto-evaluation after CSV import UPDATE
|
||||||
|
if training_type_id:
|
||||||
|
try:
|
||||||
|
# Build activity dict for evaluation
|
||||||
|
activity_dict = {
|
||||||
|
"id": existing_id,
|
||||||
|
"profile_id": pid,
|
||||||
|
"date": date,
|
||||||
|
"training_type_id": training_type_id,
|
||||||
|
"duration_min": duration_min,
|
||||||
|
"hr_avg": tf(row.get('Durchschn. Herzfrequenz (count/min)','')),
|
||||||
|
"hr_max": tf(row.get('Max. Herzfrequenz (count/min)','')),
|
||||||
|
"distance_km": tf(row.get('Distanz (km)','')),
|
||||||
|
"kcal_active": kj(row.get('Aktive Energie (kJ)','')),
|
||||||
|
"kcal_resting": kj(row.get('Ruheeinträge (kJ)','')),
|
||||||
|
"rpe": None,
|
||||||
|
"pace_min_per_km": None,
|
||||||
|
"cadence": None,
|
||||||
|
"elevation_gain": None
|
||||||
|
}
|
||||||
|
evaluate_and_save_activity(cur, existing_id, activity_dict, training_type_id, pid)
|
||||||
|
logger.debug(f"[AUTO-EVAL] Re-evaluated updated activity {existing_id}")
|
||||||
|
except Exception as eval_error:
|
||||||
|
logger.warning(f"[AUTO-EVAL] Failed to re-evaluate updated activity {existing_id}: {eval_error}")
|
||||||
else:
|
else:
|
||||||
# Insert new entry
|
# Insert new entry
|
||||||
|
new_id = str(uuid.uuid4())
|
||||||
cur.execute("""INSERT INTO activity_log
|
cur.execute("""INSERT INTO activity_log
|
||||||
(id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting,
|
(id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting,
|
||||||
hr_avg,hr_max,distance_km,source,training_type_id,training_category,training_subcategory,created)
|
hr_avg,hr_max,distance_km,source,training_type_id,training_category,training_subcategory,created)
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'apple_health',%s,%s,%s,CURRENT_TIMESTAMP)""",
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'apple_health',%s,%s,%s,CURRENT_TIMESTAMP)""",
|
||||||
(str(uuid.uuid4()),pid,date,start,row.get('End',''),wtype,duration_min,
|
(new_id,pid,date,start,row.get('End',''),wtype,duration_min,
|
||||||
kj(row.get('Aktive Energie (kJ)','')),kj(row.get('Ruheeinträge (kJ)','')),
|
kj(row.get('Aktive Energie (kJ)','')),kj(row.get('Ruheeinträge (kJ)','')),
|
||||||
tf(row.get('Durchschn. Herzfrequenz (count/min)','')),
|
tf(row.get('Durchschn. Herzfrequenz (count/min)','')),
|
||||||
tf(row.get('Max. Herzfrequenz (count/min)','')),
|
tf(row.get('Max. Herzfrequenz (count/min)','')),
|
||||||
tf(row.get('Distanz (km)','')),
|
tf(row.get('Distanz (km)','')),
|
||||||
training_type_id,training_category,training_subcategory))
|
training_type_id,training_category,training_subcategory))
|
||||||
inserted+=1
|
inserted+=1
|
||||||
|
|
||||||
|
# Phase 1.2: Auto-evaluation after CSV import INSERT
|
||||||
|
if training_type_id:
|
||||||
|
try:
|
||||||
|
# Build activity dict for evaluation
|
||||||
|
activity_dict = {
|
||||||
|
"id": new_id,
|
||||||
|
"profile_id": pid,
|
||||||
|
"date": date,
|
||||||
|
"training_type_id": training_type_id,
|
||||||
|
"duration_min": duration_min,
|
||||||
|
"hr_avg": tf(row.get('Durchschn. Herzfrequenz (count/min)','')),
|
||||||
|
"hr_max": tf(row.get('Max. Herzfrequenz (count/min)','')),
|
||||||
|
"distance_km": tf(row.get('Distanz (km)','')),
|
||||||
|
"kcal_active": kj(row.get('Aktive Energie (kJ)','')),
|
||||||
|
"kcal_resting": kj(row.get('Ruheeinträge (kJ)','')),
|
||||||
|
"rpe": None,
|
||||||
|
"pace_min_per_km": None,
|
||||||
|
"cadence": None,
|
||||||
|
"elevation_gain": None
|
||||||
|
}
|
||||||
|
evaluate_and_save_activity(cur, new_id, activity_dict, training_type_id, pid)
|
||||||
|
logger.debug(f"[AUTO-EVAL] Evaluated imported activity {new_id}")
|
||||||
|
except Exception as eval_error:
|
||||||
|
logger.warning(f"[AUTO-EVAL] Failed to evaluate imported activity {new_id}: {eval_error}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Import row failed: {e}")
|
logger.warning(f"Import row failed: {e}")
|
||||||
skipped+=1
|
skipped+=1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user