pjxcharya commited on
Commit
426f314
·
verified ·
1 Parent(s): f070cba

Upload 16 files

Browse files
app/api/__init__.py ADDED
File without changes
app/api/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (196 Bytes). View file
 
app/api/endpoints/__init__.py ADDED
File without changes
app/api/endpoints/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (206 Bytes). View file
 
app/api/endpoints/__pycache__/plans.cpython-312.pyc ADDED
Binary file (2.69 kB). View file
 
app/api/endpoints/plans.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/api/endpoints/plans.py
2
+ from fastapi import APIRouter, HTTPException
3
+ # Import the new RestartPlanBody model
4
+ from app.core.models import GeneratePlanRequest, RevisePlanRequest, PlanResponse, RestartPlanBody
5
+ from app.services.planner_service import planner_service
6
+
7
+ router = APIRouter()
8
+
9
+ # Updated endpoint to pass the new attribute
10
+ @router.post("/generate", response_model=PlanResponse)
11
+ async def generate_new_plan(request: GeneratePlanRequest):
12
+ """
13
+ Generates a new training plan based on athlete's profile and goal timeframe.
14
+ """
15
+ plan_object = await planner_service.create_plan(
16
+ profile=request.athlete_profile,
17
+ plan_type=request.plan_type,
18
+ focus=request.focus,
19
+ time_to_achieve=request.time_to_achieve # Pass the new value
20
+ )
21
+ if "error" in plan_object:
22
+ raise HTTPException(status_code=500, detail=plan_object["error"])
23
+ return plan_object
24
+
25
+ # The revise endpoint remains unchanged
26
+ @router.post("/revise", response_model=PlanResponse)
27
+ async def revise_existing_plan(request: RevisePlanRequest):
28
+ """
29
+ Revises an existing training plan based on feedback.
30
+ """
31
+ revised_plan_object = await planner_service.update_plan(
32
+ original_plan=request.original_plan,
33
+ review=request.review
34
+ )
35
+ if "error" in revised_plan_object:
36
+ raise HTTPException(status_code=500, detail=revised_plan_object["error"])
37
+ return revised_plan_object
38
+
39
+ # vvv THIS IS THE NEW ENDPOINT vvv
40
+ @router.post("/athlete/{athlete_id}/restart", response_model=PlanResponse, tags=["Training Plans"])
41
+ async def restart_athlete_plan(athlete_id: str, request: RestartPlanBody):
42
+ """
43
+ Archives the athlete's current plan and generates a new one.
44
+ This provides a 'Fresh Start' for the athlete.
45
+ """
46
+ new_plan_object = await planner_service.restart_plan(
47
+ athlete_id=athlete_id,
48
+ new_plan_details=request.model_dump()
49
+ )
50
+ if "error" in new_plan_object:
51
+ raise HTTPException(status_code=500, detail=new_plan_object["error"])
52
+
53
+ return new_plan_object
app/core/__init__.py ADDED
File without changes
app/core/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (197 Bytes). View file
 
app/core/__pycache__/config.cpython-312.pyc ADDED
Binary file (699 Bytes). View file
 
app/core/__pycache__/models.cpython-312.pyc ADDED
Binary file (2.6 kB). View file
 
app/core/config.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/core/config.py
2
+ from pydantic_settings import BaseSettings
3
+
4
+ class Settings(BaseSettings):
5
+ GEMINI_API_KEY: str
6
+
7
+ class Config:
8
+ env_file = ".env"
9
+
10
+ settings = Settings()
app/core/models.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/core/models.py
2
+ from pydantic import BaseModel, Field
3
+ from typing import Dict, List, Literal, Optional
4
+ from datetime import date
5
+
6
+ # The Task class remains deleted.
7
+
8
+ class AthleteProfile(BaseModel):
9
+ # ... (this model is unchanged)
10
+ age: int = Field(..., gt=0, description="Athlete's age")
11
+ sport: str = Field(..., min_length=1, description="Athlete's primary sport")
12
+ goal: str = Field(..., min_length=1, description="Athlete's primary training goal")
13
+
14
+ class GeneratePlanRequest(BaseModel):
15
+ # ... (this model is unchanged)
16
+ athlete_profile: AthleteProfile
17
+ plan_type: Literal["daily", "weekly"]
18
+ focus: str = Field("Overall fitness")
19
+ time_to_achieve: str
20
+
21
+ # New request model for the restart endpoint's body
22
+ class RestartPlanBody(BaseModel):
23
+ plan_type: Literal["daily", "weekly"] = Field(..., description="The plan type for the new plan")
24
+ focus: str = Field(..., description="The focus for the new plan")
25
+ time_to_achieve: str = Field(..., description="The timeframe for the new plan")
26
+
27
+
28
+ class PlanObject(BaseModel):
29
+ # ADD a status field to track the plan's state. Default is "active".
30
+ status: str = Field("active", description="The current status of the plan (e.g., active, archived_by_user)")
31
+ type: Literal["daily", "weekly"]
32
+ weekStart: date
33
+ plan: Dict[str, List[str]]
34
+ no_of_days_req: Optional[int] = None
35
+ no_of_weeks_req: Optional[int] = None
36
+
37
+ class RevisePlanRequest(BaseModel):
38
+ original_plan: PlanObject
39
+ review: str
40
+
41
+ PlanResponse = PlanObject
app/services/__init__.py ADDED
File without changes
app/services/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (201 Bytes). View file
 
app/services/__pycache__/planner_service.cpython-312.pyc ADDED
Binary file (7.43 kB). View file
 
app/services/planner_service.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/services/planner_service.py
2
+
3
+ import json
4
+ import google.generativeai as genai
5
+ from datetime import date, timedelta
6
+ from app.core.config import settings
7
+ from app.core.models import AthleteProfile, PlanObject
8
+ # In a real application, you would initialize your database client here
9
+ # For example, with Firestore:
10
+ # from google.cloud import firestore
11
+
12
+ class PlannerService:
13
+ """
14
+ Handles the core logic of creating, revising, and restarting training plans
15
+ by interacting with the Gemini AI model and a database.
16
+ """
17
+ def __init__(self, api_key: str):
18
+ """
19
+ Initializes the Gemini model and database client.
20
+ """
21
+ genai.configure(api_key=api_key)
22
+ self.model = genai.GenerativeModel('gemini-1.5-flash-latest')
23
+ # self.db = firestore.Client() # Uncomment and configure for your database
24
+
25
+ async def _parse_llm_response(self, response_text: str) -> dict:
26
+ """
27
+ Cleans and parses the string response from the LLM into a dictionary.
28
+ """
29
+ try:
30
+ # The model sometimes wraps the JSON in markdown, so we strip it.
31
+ json_string = response_text.strip().replace('```json', '').replace('```', '')
32
+ return json.loads(json_string)
33
+ except json.JSONDecodeError:
34
+ # Return a standard error object if parsing fails
35
+ return {"error": "AI failed to generate a valid plan structure. Please try again."}
36
+
37
+ def _get_start_of_week(self) -> date:
38
+ """
39
+ Calculates the date of the most recent Monday.
40
+ """
41
+ today = date.today()
42
+ # today.weekday() returns Monday as 0 and Sunday as 6
43
+ start_of_week = today - timedelta(days=today.weekday())
44
+ return start_of_week
45
+
46
+ async def create_plan(self, profile: AthleteProfile, plan_type: str, focus: str, time_to_achieve: str) -> dict:
47
+ """
48
+ Generates the data for a new, repeatable training plan using the AI.
49
+ """
50
+ prompt = f"""
51
+ You are an expert sports trainer for KhelVerse creating a repeatable, template-based training plan.
52
+
53
+ **Athlete Profile:** {profile.model_dump_json()}
54
+ **Primary Goal:** {profile.goal}
55
+ **Desired Timeframe to Achieve Goal:** {time_to_achieve}
56
+ **Plan Type Requested:** {plan_type}
57
+ **Specific Focus:** {focus}
58
+
59
+ **Your Task & Rules:**
60
+ 1. Create a single, repeatable training template. **DO NOT** create a progressive plan.
61
+ 2. Estimate the total duration needed to achieve the goal.
62
+ 3. Return a single, valid JSON object with two top-level keys: "plan" and a duration key ("no_of_days_req" or "no_of_weeks_req").
63
+
64
+ **Output Format Rules:**
65
+ - The "plan" dictionary keys depend on the plan type.
66
+ - The value for each key **MUST BE A LIST OF STRINGS**. Each string should be a single, specific exercise.
67
+ - **If `plan_type` is "daily"**: Keys are times (e.g., "6 AM"). Example value: ["10 min jogging", "Dynamic stretching"].
68
+ - **If `plan_type` is "weekly"**: Keys are "D1" through "D7". Example value for "D1": ["Run 5km at moderate pace", "Core workout: 3 sets of planks"].
69
+ """
70
+ response = await self.model.generate_content_async(prompt)
71
+ llm_response = await self._parse_llm_response(response.text)
72
+
73
+ if "error" in llm_response:
74
+ return llm_response
75
+
76
+ # Assemble the full object to be returned by the API
77
+ full_plan_object = {
78
+ "type": plan_type,
79
+ "weekStart": self._get_start_of_week(),
80
+ "plan": llm_response.get('plan', {}),
81
+ "no_of_days_req": llm_response.get('no_of_days_req'),
82
+ "no_of_weeks_req": llm_response.get('no_of_weeks_req')
83
+ }
84
+ return full_plan_object
85
+
86
+ async def update_plan(self, original_plan: PlanObject, review: str) -> dict:
87
+ """
88
+ Revises an existing plan based on user feedback.
89
+ """
90
+ # Dump the model to a dict to get a clean JSON representation for the prompt
91
+ plan_to_revise_for_prompt = original_plan.model_dump()['plan']
92
+
93
+ prompt = f"""
94
+ You are an expert sports trainer. Revise the tasks in the following repeatable training template based on the user's review.
95
+
96
+ **Original Plan Template (JSON format):**
97
+ {json.dumps(plan_to_revise_for_prompt, indent=2)}
98
+
99
+ **User's Review:**
100
+ "{review}"
101
+
102
+ **Your Task & Rules:**
103
+ 1. Modify the plan template based ONLY on the review.
104
+ 2. The value for each key MUST remain a LIST OF STRINGS.
105
+ 3. The output must be a single, valid JSON object of the tasks, maintaining the original key structure.
106
+ """
107
+ response = await self.model.generate_content_async(prompt)
108
+ revised_plan_dict = await self._parse_llm_response(response.text)
109
+
110
+ if "error" in revised_plan_dict:
111
+ return revised_plan_dict
112
+
113
+ # Reconstruct the full object, carrying over metadata from the original plan
114
+ full_revised_object = {
115
+ "status": original_plan.status, # Keep original status
116
+ "type": original_plan.type,
117
+ "weekStart": original_plan.weekStart,
118
+ "plan": revised_plan_dict,
119
+ "no_of_days_req": original_plan.no_of_days_req,
120
+ "no_of_weeks_req": original_plan.no_of_weeks_req
121
+ }
122
+ return full_revised_object
123
+
124
+ async def restart_plan(self, athlete_id: str, new_plan_details: dict) -> dict:
125
+ """
126
+ Orchestrates the 'Fresh Start' feature: archives the old plan and creates a new one.
127
+ """
128
+ # --- Step 1: Archive the old plan (Database Interaction) ---
129
+ print(f"LOGIC: Searching for active plan for athlete {athlete_id} to archive.")
130
+ # This is where you would put your actual database code.
131
+ # Example for Firestore:
132
+ # plans_ref = self.db.collection('plans')
133
+ # query = plans_ref.where('athleteId', '==', athlete_id).where('status', '==', 'active').limit(1)
134
+ # for plan_doc in query.stream():
135
+ # print(f"LOGIC: Found plan {plan_doc.id}. Setting status to 'archived_by_user'.")
136
+ # plan_doc.reference.update({'status': 'archived_by_user'})
137
+
138
+
139
+ # --- Step 2: Generate the new plan (Re-using existing logic) ---
140
+ print("LOGIC: Generating a new plan.")
141
+
142
+ # Fetch the full athlete profile from your database using the athlete_id
143
+ # MOCKING athlete profile for this example. Replace with your DB call.
144
+ athlete_profile_data = {"age": 25, "sport": "Running", "goal": "Run a marathon"}
145
+ athlete_profile = AthleteProfile(**athlete_profile_data)
146
+
147
+ # Call the existing create_plan method to get the new plan's data dictionary
148
+ new_plan_dict = await self.create_plan(
149
+ profile=athlete_profile,
150
+ plan_type=new_plan_details['plan_type'],
151
+ focus=new_plan_details['focus'],
152
+ time_to_achieve=new_plan_details['time_to_achieve']
153
+ )
154
+
155
+ # In a real app, you would save the new plan to the database here.
156
+ # new_plan_dict['athleteId'] = athlete_id # Add athlete ID before saving
157
+ # new_plan_ref = self.db.collection('plans').add(new_plan_dict)
158
+ # print(f"LOGIC: New plan created in DB with ID: {new_plan_ref[1].id}")
159
+
160
+ return new_plan_dict
161
+
162
+ # A single instance of the service is created to be used across the application
163
+ planner_service = PlannerService(api_key=settings.GEMINI_API_KEY)