mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-04-04 20:13:08 +00:00
sync
This commit is contained in:
35
backend/app/routes/gpx.py
Normal file
35
backend/app/routes/gpx.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.services.gpx import parse_gpx, store_gpx_file
|
||||
from app.schemas.gpx import RouteCreate, Route as RouteSchema
|
||||
from app.models import Route
|
||||
import os
|
||||
|
||||
router = APIRouter(prefix="/gpx", tags=["GPX Routes"])
|
||||
|
||||
@router.post("/upload", response_model=RouteSchema)
|
||||
async def upload_gpx_route(
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
# Store GPX file
|
||||
gpx_path = await store_gpx_file(file)
|
||||
|
||||
# Parse GPX file
|
||||
gpx_data = await parse_gpx(gpx_path)
|
||||
|
||||
# Create route in database
|
||||
route_data = RouteCreate(
|
||||
name=file.filename,
|
||||
description=f"Uploaded from {file.filename}",
|
||||
total_distance=gpx_data['total_distance'],
|
||||
elevation_gain=gpx_data['elevation_gain'],
|
||||
gpx_file_path=gpx_path
|
||||
)
|
||||
db_route = Route(**route_data.dict())
|
||||
db.add(db_route)
|
||||
await db.commit()
|
||||
await db.refresh(db_route)
|
||||
|
||||
return db_route
|
||||
89
backend/app/routes/plan.py
Normal file
89
backend/app/routes/plan.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.database import get_db
|
||||
from app.models import Plan, PlanRule, Rule
|
||||
from app.schemas.plan import PlanCreate, Plan as PlanSchema
|
||||
from uuid import UUID
|
||||
|
||||
router = APIRouter(prefix="/plans", tags=["Training Plans"])
|
||||
|
||||
@router.post("/", response_model=PlanSchema)
|
||||
async def create_plan(
|
||||
plan: PlanCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
# Create plan
|
||||
db_plan = Plan(
|
||||
user_id=plan.user_id,
|
||||
start_date=plan.start_date,
|
||||
end_date=plan.end_date,
|
||||
goal=plan.goal
|
||||
)
|
||||
db.add(db_plan)
|
||||
await db.flush() # Flush to get plan ID
|
||||
|
||||
# Add rules to plan
|
||||
for rule_id in plan.rule_ids:
|
||||
db_plan_rule = PlanRule(plan_id=db_plan.id, rule_id=rule_id)
|
||||
db.add(db_plan_rule)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_plan)
|
||||
return db_plan
|
||||
|
||||
@router.get("/{plan_id}", response_model=PlanSchema)
|
||||
async def read_plan(
|
||||
plan_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
plan = await db.get(Plan, plan_id)
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="Plan not found")
|
||||
return plan
|
||||
|
||||
@router.get("/", response_model=list[PlanSchema])
|
||||
async def read_plans(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(Plan))
|
||||
return result.scalars().all()
|
||||
|
||||
@router.put("/{plan_id}", response_model=PlanSchema)
|
||||
async def update_plan(
|
||||
plan_id: UUID,
|
||||
plan: PlanCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
db_plan = await db.get(Plan, plan_id)
|
||||
if not db_plan:
|
||||
raise HTTPException(status_code=404, detail="Plan not found")
|
||||
|
||||
# Update plan fields
|
||||
db_plan.user_id = plan.user_id
|
||||
db_plan.start_date = plan.start_date
|
||||
db_plan.end_date = plan.end_date
|
||||
db_plan.goal = plan.goal
|
||||
|
||||
# Update rules
|
||||
await db.execute(PlanRule.delete().where(PlanRule.plan_id == plan_id))
|
||||
for rule_id in plan.rule_ids:
|
||||
db_plan_rule = PlanRule(plan_id=plan_id, rule_id=rule_id)
|
||||
db.add(db_plan_rule)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_plan)
|
||||
return db_plan
|
||||
|
||||
@router.delete("/{plan_id}")
|
||||
async def delete_plan(
|
||||
plan_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
plan = await db.get(Plan, plan_id)
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="Plan not found")
|
||||
|
||||
await db.delete(plan)
|
||||
await db.commit()
|
||||
return {"detail": "Plan deleted"}
|
||||
79
backend/app/routes/prompts.py
Normal file
79
backend/app/routes/prompts.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.prompt import Prompt
|
||||
from app.schemas.prompt import Prompt as PromptSchema, PromptCreate, PromptUpdate
|
||||
from app.services.prompt_manager import PromptManager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[PromptSchema])
|
||||
async def read_prompts(db: AsyncSession = Depends(get_db)):
|
||||
"""Get all prompts."""
|
||||
result = await db.execute(select(Prompt))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/{prompt_id}", response_model=PromptSchema)
|
||||
async def read_prompt(prompt_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a specific prompt by ID."""
|
||||
prompt = await db.get(Prompt, prompt_id)
|
||||
if not prompt:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found")
|
||||
return prompt
|
||||
|
||||
|
||||
@router.post("/", response_model=PromptSchema)
|
||||
async def create_prompt(
|
||||
prompt: PromptCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create a new prompt version."""
|
||||
prompt_manager = PromptManager(db)
|
||||
new_prompt = await prompt_manager.create_prompt_version(
|
||||
action_type=prompt.action_type,
|
||||
prompt_text=prompt.prompt_text,
|
||||
model=prompt.model
|
||||
)
|
||||
return new_prompt
|
||||
|
||||
|
||||
@router.get("/active/{action_type}")
|
||||
async def get_active_prompt(
|
||||
action_type: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get the active prompt for a specific action type."""
|
||||
prompt_manager = PromptManager(db)
|
||||
prompt_text = await prompt_manager.get_active_prompt(action_type)
|
||||
if not prompt_text:
|
||||
raise HTTPException(status_code=404, detail=f"No active prompt found for {action_type}")
|
||||
return {"action_type": action_type, "prompt_text": prompt_text}
|
||||
|
||||
|
||||
@router.get("/history/{action_type}", response_model=List[PromptSchema])
|
||||
async def get_prompt_history(
|
||||
action_type: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get the version history for a specific action type."""
|
||||
prompt_manager = PromptManager(db)
|
||||
prompts = await prompt_manager.get_prompt_history(action_type)
|
||||
return prompts
|
||||
|
||||
|
||||
@router.post("/{prompt_id}/activate")
|
||||
async def activate_prompt_version(
|
||||
prompt_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Activate a specific prompt version."""
|
||||
prompt_manager = PromptManager(db)
|
||||
success = await prompt_manager.activate_prompt_version(prompt_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found")
|
||||
return {"message": "Prompt version activated successfully"}
|
||||
66
backend/app/routes/rule.py
Normal file
66
backend/app/routes/rule.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.models import Rule
|
||||
from app.schemas.rule import RuleCreate, Rule as RuleSchema
|
||||
from uuid import UUID
|
||||
|
||||
router = APIRouter(prefix="/rules", tags=["Rules"])
|
||||
|
||||
@router.post("/", response_model=RuleSchema)
|
||||
async def create_rule(
|
||||
rule: RuleCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
db_rule = Rule(**rule.dict())
|
||||
db.add(db_rule)
|
||||
await db.commit()
|
||||
await db.refresh(db_rule)
|
||||
return db_rule
|
||||
|
||||
@router.get("/{rule_id}", response_model=RuleSchema)
|
||||
async def read_rule(
|
||||
rule_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
rule = await db.get(Rule, rule_id)
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="Rule not found")
|
||||
return rule
|
||||
|
||||
@router.get("/", response_model=list[RuleSchema])
|
||||
async def read_rules(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(sa.select(Rule))
|
||||
return result.scalars().all()
|
||||
|
||||
@router.put("/{rule_id}", response_model=RuleSchema)
|
||||
async def update_rule(
|
||||
rule_id: UUID,
|
||||
rule: RuleCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
db_rule = await db.get(Rule, rule_id)
|
||||
if not db_rule:
|
||||
raise HTTPException(status_code=404, detail="Rule not found")
|
||||
|
||||
for key, value in rule.dict().items():
|
||||
setattr(db_rule, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_rule)
|
||||
return db_rule
|
||||
|
||||
@router.delete("/{rule_id}")
|
||||
async def delete_rule(
|
||||
rule_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
rule = await db.get(Rule, rule_id)
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="Rule not found")
|
||||
|
||||
await db.delete(rule)
|
||||
await db.commit()
|
||||
return {"detail": "Rule deleted"}
|
||||
138
backend/app/routes/workouts.py
Normal file
138
backend/app/routes/workouts.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.workout import Workout
|
||||
from app.models.analysis import Analysis
|
||||
from app.models.garmin_sync_log import GarminSyncLog
|
||||
from app.models.plan import Plan
|
||||
from app.schemas.workout import Workout as WorkoutSchema, WorkoutSyncStatus
|
||||
from app.schemas.analysis import Analysis as AnalysisSchema
|
||||
from app.services.workout_sync import WorkoutSyncService
|
||||
from app.services.ai_service import AIService
|
||||
from app.services.plan_evolution import PlanEvolutionService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[WorkoutSchema])
|
||||
async def read_workouts(db: AsyncSession = Depends(get_db)):
|
||||
"""Get all workouts."""
|
||||
result = await db.execute(select(Workout))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/{workout_id}", response_model=WorkoutSchema)
|
||||
async def read_workout(workout_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a specific workout by ID."""
|
||||
workout = await db.get(Workout, workout_id)
|
||||
if not workout:
|
||||
raise HTTPException(status_code=404, detail="Workout not found")
|
||||
return workout
|
||||
|
||||
|
||||
@router.post("/sync")
|
||||
async def trigger_garmin_sync(
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Trigger background sync of recent Garmin activities."""
|
||||
sync_service = WorkoutSyncService(db)
|
||||
background_tasks.add_task(sync_service.sync_recent_activities, days_back=14)
|
||||
return {"message": "Garmin sync started"}
|
||||
|
||||
|
||||
@router.get("/sync-status", response_model=WorkoutSyncStatus)
|
||||
async def get_sync_status(db: AsyncSession = Depends(get_db)):
|
||||
"""Get the latest sync status."""
|
||||
result = await db.execute(
|
||||
select(GarminSyncLog).order_by(GarminSyncLog.created_at.desc()).limit(1)
|
||||
)
|
||||
sync_log = result.scalar_one_or_none()
|
||||
if not sync_log:
|
||||
return WorkoutSyncStatus(status="never_synced")
|
||||
return sync_log
|
||||
|
||||
|
||||
@router.post("/{workout_id}/analyze")
|
||||
async def analyze_workout(
|
||||
workout_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Trigger AI analysis of a specific workout."""
|
||||
workout = await db.get(Workout, workout_id)
|
||||
if not workout:
|
||||
raise HTTPException(status_code=404, detail="Workout not found")
|
||||
|
||||
ai_service = AIService(db)
|
||||
background_tasks.add_task(
|
||||
analyze_and_store_workout,
|
||||
db, workout, ai_service
|
||||
)
|
||||
|
||||
return {"message": "Analysis started", "workout_id": workout_id}
|
||||
|
||||
|
||||
async def analyze_and_store_workout(db: AsyncSession, workout: Workout, ai_service: AIService):
|
||||
"""Background task to analyze workout and store results."""
|
||||
try:
|
||||
# Get current plan if workout is associated with one
|
||||
plan = None
|
||||
if workout.plan_id:
|
||||
plan = await db.get(Plan, workout.plan_id)
|
||||
|
||||
# Analyze workout
|
||||
analysis_result = await ai_service.analyze_workout(workout, plan.jsonb_plan if plan else None)
|
||||
|
||||
# Store analysis
|
||||
analysis = Analysis(
|
||||
workout_id=workout.id,
|
||||
jsonb_feedback=analysis_result.get("feedback", {}),
|
||||
suggestions=analysis_result.get("suggestions", {})
|
||||
)
|
||||
db.add(analysis)
|
||||
await db.commit()
|
||||
|
||||
except Exception as e:
|
||||
# Log error but don't crash the background task
|
||||
print(f"Error analyzing workout {workout.id}: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{workout_id}/analyses", response_model=List[AnalysisSchema])
|
||||
async def read_workout_analyses(workout_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Get all analyses for a specific workout."""
|
||||
workout = await db.get(Workout, workout_id)
|
||||
if not workout:
|
||||
raise HTTPException(status_code=404, detail="Workout not found")
|
||||
|
||||
return workout.analyses
|
||||
|
||||
|
||||
@router.post("/analyses/{analysis_id}/approve")
|
||||
async def approve_analysis(
|
||||
analysis_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Approve analysis suggestions and trigger plan evolution."""
|
||||
analysis = await db.get(Analysis, analysis_id)
|
||||
if not analysis:
|
||||
raise HTTPException(status_code=404, detail="Analysis not found")
|
||||
|
||||
analysis.approved = True
|
||||
|
||||
# Trigger plan evolution if suggestions exist and workout has a plan
|
||||
if analysis.suggestions and analysis.workout.plan_id:
|
||||
evolution_service = PlanEvolutionService(db)
|
||||
current_plan = await db.get(Plan, analysis.workout.plan_id)
|
||||
if current_plan:
|
||||
new_plan = await evolution_service.evolve_plan_from_analysis(
|
||||
analysis, current_plan
|
||||
)
|
||||
await db.commit()
|
||||
return {"message": "Analysis approved", "new_plan_id": new_plan.id if new_plan else None}
|
||||
|
||||
await db.commit()
|
||||
return {"message": "Analysis approved"}
|
||||
Reference in New Issue
Block a user