File size: 23,348 Bytes
28e5e1b
 
 
a06906b
28e5e1b
 
c3cf389
28e5e1b
 
48f2ece
 
5c2a185
28e5e1b
a06906b
 
28e5e1b
 
 
 
 
 
 
 
c3cf389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a06906b
 
c3cf389
 
a06906b
c3cf389
 
 
a06906b
c3cf389
 
 
a06906b
c3cf389
67945d8
a06906b
 
 
67945d8
 
 
a06906b
 
 
 
 
 
 
 
 
 
 
 
67945d8
 
 
a06906b
c3cf389
 
 
 
a06906b
 
 
 
 
67945d8
a06906b
 
 
 
c3cf389
 
 
67945d8
c3cf389
67945d8
c3cf389
 
 
 
a06906b
67945d8
c3cf389
a06906b
 
 
 
c3cf389
67945d8
 
a06906b
c3cf389
a06906b
c3cf389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67945d8
 
 
 
 
 
c3cf389
 
 
 
 
 
 
 
28e5e1b
 
 
 
 
5c2a185
28e5e1b
 
 
 
 
a06906b
 
 
 
 
5c2a185
a06906b
5c2a185
a06906b
28e5e1b
5c2a185
a06906b
5c2a185
a06906b
28e5e1b
c3cf389
a06906b
c3cf389
a06906b
 
b3f2162
 
 
 
 
a06906b
c3cf389
a06906b
c3cf389
28e5e1b
a06906b
28e5e1b
 
c3cf389
 
 
 
 
 
28e5e1b
 
 
 
c3cf389
 
 
 
 
a06906b
c3cf389
 
a06906b
c3cf389
a06906b
c3cf389
28e5e1b
 
a06906b
28e5e1b
 
 
5c2a185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3cf389
 
 
 
 
 
 
 
 
 
 
5c2a185
 
 
 
 
c3cf389
b3f2162
 
c3cf389
b3f2162
 
 
 
 
 
 
 
 
 
c3cf389
b3f2162
c3cf389
 
 
 
 
 
 
 
b3f2162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c2a185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3f2162
 
5c2a185
b3f2162
5c2a185
 
 
 
 
28e5e1b
 
 
 
 
4379b09
67945d8
 
 
 
 
4379b09
28e5e1b
67945d8
 
 
 
 
 
 
 
 
 
 
 
 
c3cf389
67945d8
c3cf389
67945d8
 
 
 
 
 
 
 
 
 
 
 
 
28e5e1b
c3cf389
 
 
 
 
5c2a185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3cf389
 
 
 
 
 
 
 
4379b09
5c2a185
b3f2162
 
 
 
 
 
 
 
 
 
5c2a185
 
 
 
 
 
 
 
 
 
 
 
 
28e5e1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
"""
Fitness agent tools with grouped function and prompt definitions.
"""
import logging
from typing import Optional, Any, Dict, List
from dataclasses import dataclass
from datetime import date, timedelta
from agents import function_tool, RunContextWrapper

from .fitness_plan_models import FitnessPlan
from .training_plan_models import TrainingDay, IntensityLevel
from .user_session import SessionManager, UserProfile

logger = logging.getLogger(__name__)


@dataclass
class FunctionToolConfig:
    """Configuration for a function tool including its prompt instructions."""
    function: callable
    prompt_instructions: str


@dataclass
class ScheduledTrainingDay:
    """A training day with an assigned date."""
    date: date
    training_day: TrainingDay
    split_name: str
    week_number: int
    day_in_week: int


def build_fitness_schedule(fitness_plan: FitnessPlan, start_date: Optional[date] = None) -> List[ScheduledTrainingDay]:
    """
    Build a date-based schedule from a fitness plan.
    
    Args:
        fitness_plan: The FitnessPlan object to create a schedule from
        start_date: Optional start date, defaults to fitness_plan.start_date or today
    
    Returns:
        List of ScheduledTrainingDay objects with assigned dates
    """
    logger.info(f"Building fitness schedule for plan: {fitness_plan.name}")
    
    if start_date is None:
        start_date = fitness_plan.start_date or date.today()
    logger.info(f"Schedule start date: {start_date}")
    
    # Use target_date as end_date if available
    end_date = fitness_plan.target_date
    logger.info(f"Schedule end date: {end_date}")
    
    schedule = []
    current_date = start_date
    logger.info(f"Training plan has {len(fitness_plan.training_plan.training_periods)} periods")
    
    # Process training periods in order
    for period_idx, period in enumerate(fitness_plan.training_plan.training_periods):
        logger.info(f"Processing period {period_idx + 1}: {period.name}")
        
        # Use period's start_date if provided, otherwise use current_date
        period_start_date = period.start_date if period.start_date else current_date
        period_current_date = period_start_date
        logger.info(f"Period {period_idx + 1} starts on: {period_start_date}")
        
        # Determine this period's end date
        next_period = None
        if period_idx < len(fitness_plan.training_plan.training_periods) - 1:
            next_period = fitness_plan.training_plan.training_periods[period_idx + 1]
            period_end_date = next_period.start_date - timedelta(days=1)
            logger.info(f"Period {period_idx + 1} ends on: {period_end_date} (before next period)")
        else:
            # Last period goes until the plan's target date
            period_end_date = end_date
            logger.info(f"Period {period_idx + 1} ends on: {period_end_date} (plan end date)")
        
        # Get the training split for this period
        training_split = period.training_split
        logger.info(f"Period {period_idx + 1} using split: {training_split.name} with {len(training_split.training_days)} days")
        
        # Calculate how many days this split's cycle is
        week_number = 1
        
        # Continue cycling through the split until we reach the period end date or plan end date
        while (period_end_date is None or period_current_date <= period_end_date) and (end_date is None or period_current_date <= end_date):
            logger.debug(f"Starting week {week_number} of {training_split.name} on {period_current_date}")
            cycle_completed = True
            
            for day_idx, training_day in enumerate(training_split.training_days):
                # Stop if we've reached the period end date or plan end date
                if (period_end_date and period_current_date > period_end_date) or (end_date and period_current_date > end_date):
                    logger.debug(f"Stopping at {period_current_date} - reached end date")
                    cycle_completed = False
                    break
                    
                scheduled_day = ScheduledTrainingDay(
                    date=period_current_date,
                    training_day=training_day,
                    split_name=training_split.name,
                    week_number=week_number,
                    day_in_week=day_idx + 1
                )
                schedule.append(scheduled_day)
                logger.debug(f"Scheduled {training_day.name} on {period_current_date} (Week {week_number}, Day {day_idx + 1})")
                period_current_date += timedelta(days=1)
            
            # Only increment week number if we completed a full cycle
            if cycle_completed:
                week_number += 1
                logger.debug(f"Completed full cycle, moving to week {week_number}")
        
        # Update current_date for the next period
        current_date = period_current_date
        logger.info(f"Period {period_idx + 1} completed. Next period will start from: {current_date}")
    
    logger.info(f"Schedule built successfully with {len(schedule)} scheduled days")
    return schedule


def format_schedule_summary(schedule: List[ScheduledTrainingDay], days_to_show: int = 14) -> str:
    """
    Format the schedule into a readable summary showing upcoming training days.
    
    Args:
        schedule: List of ScheduledTrainingDay objects
        days_to_show: Number of upcoming days to display
    
    Returns:
        Formatted string showing the schedule
    """
    if not schedule:
        return "No scheduled training days found."
    
    # Show only upcoming days (from today forward)
    today = date.today()
    upcoming_days = [day for day in schedule if day.date >= today][:days_to_show]
    
    if not upcoming_days:
        return "No upcoming training days scheduled."
    
    summary_lines = ["**Upcoming Training Schedule:**"]
    current_week = None
    
    for scheduled_day in upcoming_days:
        # Add week separator
        week_key = f"{scheduled_day.split_name} - Week {scheduled_day.week_number}"
        if week_key != current_week:
            summary_lines.append(f"\n*{week_key}*")
            current_week = week_key
        
        # Format the day
        day_str = scheduled_day.date.strftime("%a, %b %d")
        intensity_str = f" ({scheduled_day.training_day.intensity.value})" if scheduled_day.training_day.intensity else ""
        
        # Check if this is a rest day (no exercises or rest intensity)
        is_rest_day = (not scheduled_day.training_day.exercises or 
                      len(scheduled_day.training_day.exercises) == 0 or
                      (scheduled_day.training_day.intensity and scheduled_day.training_day.intensity.value == "rest"))
        
        if is_rest_day:
            summary_lines.append(f"β€’ {day_str}: {scheduled_day.training_day.name}")
        else:
            exercise_count = len(scheduled_day.training_day.exercises) if scheduled_day.training_day.exercises else 0
            summary_lines.append(f"β€’ {day_str}: {scheduled_day.training_day.name}{intensity_str} ({exercise_count} exercises)")
    
    return "\n".join(summary_lines)


@function_tool
async def create_fitness_plan(
    ctx: RunContextWrapper[Any],
    fitness_plan: FitnessPlan,
) -> str:
    """Save a completed fitness plan to the user session for display in the UI.

    Args:
        fitness_plan: A fully completed FitnessPlan object with name, training_plan, and meal_plan
    """
    try:
        logger.info(f"Creating fitness plan: {fitness_plan.name}")
        logger.info(f"Plan goal: {fitness_plan.goal}")
        logger.info(f"Plan start date: {fitness_plan.start_date}")
        logger.info(f"Plan target date: {fitness_plan.target_date}")
        
        # Get or create user session
        logger.debug("Getting or creating user session")
        session = SessionManager.get_or_create_session()
        logger.info(f"User session obtained: {session.session_id}")
        
        # Save the fitness plan to the user session
        logger.debug("Saving fitness plan to user session")
        session.set_fitness_plan(fitness_plan)
        logger.info("Fitness plan saved to session successfully")
        
        # Build the schedule from the fitness plan
        logger.info("Building fitness schedule from plan")
        schedule = build_fitness_schedule(fitness_plan)
        logger.info(f"Schedule built with {len(schedule)} days")
        
        # Save the schedule to the user session
        logger.debug("Saving schedule to user session")
        session.set_schedule(schedule)
        logger.info("Schedule saved to session successfully")
        
        logger.debug("Formatting schedule summary")
        schedule_summary = format_schedule_summary(schedule)
        logger.debug("Schedule summary formatted successfully")
        
        # Format the plan for display
        logger.debug("Formatting plan for display")
        formatted_plan = f"""**{fitness_plan.name}**

**Goal:** {fitness_plan.goal}

**Training Plan:** {fitness_plan.training_plan.name}
{fitness_plan.training_plan.description}

{schedule_summary}

**Meal Plan:**
{fitness_plan.meal_plan}"""
        
        # Calculate duration for display
        if fitness_plan.target_date and fitness_plan.start_date:
            duration_days = (fitness_plan.target_date - fitness_plan.start_date).days
            duration_weeks = duration_days // 7
            duration_text = f"{duration_weeks}-week"
            logger.info(f"Plan duration: {duration_days} days ({duration_weeks} weeks)")
        else:
            duration_text = "customized"
            logger.info("Plan duration: customized (no specific dates provided)")
        
        logger.info("Fitness plan creation completed successfully")
        return f"I've created and saved your personalized fitness plan with a {duration_text} schedule:\n\n{formatted_plan}\n\nThis plan has been tailored specifically for your requirements and is now available in the fitness plan section below. Please consult with a healthcare provider before starting any new fitness program."
        
    except Exception as e:
        logger.error(f"Error creating fitness plan: {str(e)}", exc_info=True)
        return f"I apologize, but I encountered an error while saving your fitness plan: {str(e)}. Please try again or contact support if the issue persists."


@function_tool
async def get_user_profile(
    ctx: RunContextWrapper[Any],
) -> str:
    """Get the current user's profile information.

    Returns:
        String describing the user's profile or a message if no profile exists
    """
    try:
        session = SessionManager.get_current_session()
        
        if not session:
            return "No user session found. Your profile information will be saved as we talk."
        
        profile = session.profile
        
        if not any([profile.name, profile.age, profile.fitness_level, profile.goals]):
            return "No profile information saved yet. Tell me about yourself - your fitness level, goals, and preferences - and I'll remember them for future conversations."
        
        profile_info = []
        if profile.name:
            profile_info.append(f"Name: {profile.name}")
        if profile.age:
            profile_info.append(f"Age: {profile.age}")
        if profile.fitness_level:
            profile_info.append(f"Fitness Level: {profile.fitness_level}")
        if profile.goals:
            profile_info.append(f"Goals: {', '.join(profile.goals)}")
        if profile.equipment_available:
            profile_info.append(f"Available Equipment: {', '.join(profile.equipment_available)}")
        
        return "Here's your current profile:\n\n" + "\n".join(profile_info)
        
    except Exception as e:
        return f"I encountered an error while retrieving your profile: {str(e)}. Please try again."


@function_tool
async def update_user_profile(
    ctx: RunContextWrapper[Any],
    name: Optional[str] = None,
    age: Optional[int] = None,
    fitness_level: Optional[str] = None,
    goals: Optional[List[str]] = None,
    equipment_available: Optional[List[str]] = None,
) -> str:
    """Update the user's profile information.

    Args:
        name: User's name
        age: User's age
        fitness_level: User's fitness level (beginner, intermediate, advanced)
        goals: List of fitness goals
        equipment_available: List of available equipment
    """
    try:
        session = SessionManager.get_or_create_session()
        
        update_data = {}
        if name is not None:
            update_data['name'] = name
        if age is not None:
            update_data['age'] = age
        if fitness_level is not None:
            update_data['fitness_level'] = fitness_level
        if goals is not None:
            update_data['goals'] = goals
        if equipment_available is not None:
            update_data['equipment_available'] = equipment_available
        
        session.update_profile(**update_data)
        
        # Note: In a production system, you might want to notify the agent
        # instance that the profile has been updated so it can refresh its context
        
        return "I've updated your profile information. This will help me create better personalized fitness plans for you."
        
    except Exception as e:
        return f"I encountered an error while updating your profile: {str(e)}. Please try again."


@function_tool
async def get_training_schedule(
    ctx: RunContextWrapper[Any],
    days_ahead: int = 14,
) -> str:
    """Get the upcoming training schedule from the current fitness plan.

    Args:
        days_ahead: Number of days ahead to show in the schedule (default: 14)
    """
    try:
        # Get the current user session
        session = SessionManager.get_current_session()
        
        if not session:
            return "No user session found. Please create a fitness plan first."
        
        # First try to get the stored schedule
        schedule = session.get_schedule()
        
        if not schedule:
            # If no stored schedule, try to build it from the fitness plan
            fitness_plan = session.get_fitness_plan()
            
            if not fitness_plan:
                return "No fitness plan is currently available. Please create a fitness plan first."
            
            # Build and store the schedule
            schedule = build_fitness_schedule(fitness_plan)
            session.set_schedule(schedule)
        
        # Format the schedule
        schedule_summary = format_schedule_summary(schedule, days_ahead)
        
        return f"Here's your upcoming training schedule:\n\n{schedule_summary}"
        
    except Exception as e:
        return f"I encountered an error while retrieving your training schedule: {str(e)}. Please try again."


@function_tool
async def get_schedule_data(
    ctx: RunContextWrapper[Any],
) -> List[ScheduledTrainingDay]:
    """Get the raw training schedule data for calendar display.

    Returns:
        List of ScheduledTrainingDay objects for calendar interface
    """
    try:
        # Get the current user session
        session = SessionManager.get_current_session()
        
        if not session:
            raise Exception("No user session found")
        
        # Get the stored schedule
        schedule = session.get_schedule()
        
        if not schedule:
            # If no stored schedule, try to build it from the fitness plan
            fitness_plan = session.get_fitness_plan()
            
            if not fitness_plan:
                raise Exception("No fitness plan available")
            
            # Build and store the schedule
            schedule = build_fitness_schedule(fitness_plan)
            session.set_schedule(schedule)
        
        return schedule
        
    except Exception as e:
        logger.error(f"Error getting schedule data: {str(e)}", exc_info=True)
        return []


@function_tool
async def clear_user_session(
    ctx: RunContextWrapper[Any],
) -> str:
    """Clear all user session data including profile and fitness plans.

    Returns:
        Confirmation message
    """
    try:
        session = SessionManager.get_current_session()
        
        if not session:
            return "No user session found to clear."
        
        # Clear all user data including schedule
        session.clear_all_data()
        
        return "I've cleared all your session data including your profile, fitness plans, and training schedule. We can start fresh whenever you're ready!"
        
    except Exception as e:
        return f"I encountered an error while clearing your session: {str(e)}. Please try again."


# Tool configurations with their associated prompt instructions
FITNESS_TOOLS = {
    "create_fitness_plan": FunctionToolConfig(
        function=create_fitness_plan,
        prompt_instructions="""
        When the user requests a fitness plan, use the create_fitness_plan tool with a fully completed FitnessPlan object.

        Fitness Plans are made up of Training Plans and Meal Plans that have a start and end date.
        If no start date is specified, assume the plan starts today.
        If no end date is given by the user assume they want a plan to be in better shape in 3 months.

        The FitnessPlan object must include:
        - name: A descriptive name for the fitness plan
        - goal: The primary fitness goal (should be specific and measurable)
        - description: Comprehensive overview of the plan and expected outcomes
        - training_plan: A TrainingPlan object with periodized phases that includes:
          * name: Name that reflects the plan's focus and target event
          * description: Comprehensive overview including training philosophy, periodization strategy, target audience, expected timeline, and how the phases progress toward the goal
          * training_periods: Ordered list of TrainingPeriod objects representing different phases:
            - Each period should have a name (e.g., 'Base Phase', 'Advanced Phase', 'Preparation Phase')
            - Include start_date for each period
            - Include intensity level (rest, light, moderate, heavy, max_effort)
            - Each period contains a training_split with training_days that include exercises
            - Follow periodization principles: typically base building β†’ peak training β†’ recovery for optimum performance at events
            - Each phase should build on the previous with appropriate progression
        - meal_plan: Detailed nutrition guidance including meal suggestions, macronutrient targets, and eating schedule. Should be practical and specific to the fitness goals.
        - start_date: When the plan should begin (defaults to today)
        - target_date: The target completion date or milestone date for the fitness plan

        Training Structure:
        - TrainingPeriod contains a TrainingSplit
        - TrainingSplit contains multiple TrainingDay objects
        - TrainingDay contains multiple Exercise objects
        - Each Exercise should have name, description, and appropriate fields (sets, reps, duration, distance, intensity)
        - Include rest days in the training split as needed

        Create periodized plans that progress intelligently toward the goal:
        - For events (hikes, competitions): Use base building β†’ strength β†’ peak β†’ taper progression
        - For aesthetic goals (summer body): Use muscle building β†’ strength β†’ cutting/definition phases
        - For general fitness: Use base building β†’ strength β†’ maintenance cycles
        
        This tool automatically builds a date-based schedule from the periodized phases and saves the plan.

        Do not read the plan back to the user in the conversation. The user can already see it in the UI component.

        In one or two sentences, let the user know the plan has been created, and ask the user if they want to make any adjustments.
        """
    ),
    "get_user_profile": FunctionToolConfig(
        function=get_user_profile,
        prompt_instructions="""
        Use this tool only when:
        1. The user explicitly asks to see their profile information
        2. You need to refresh your memory in very long conversations (15+ exchanges)
        3. You suspect the user's profile may have been updated since the conversation started
        
        In most cases, you should already have the user's profile information available in your system context.
        This tool is primarily for displaying profile information to the user or refreshing context in extended conversations.
        """
    ),
    "update_user_profile": FunctionToolConfig(
        function=update_user_profile,
        prompt_instructions="""
        Use this tool when the user provides information about themselves that should be saved for future reference.
        
        This includes their name, age, fitness level (beginner/intermediate/advanced), goals, or available equipment.
        
        You can update any combination of profile fields - only pass the parameters that have new information.
        """
    ),
    "get_training_schedule": FunctionToolConfig(
        function=get_training_schedule,
        prompt_instructions="""
        Use this tool when the user asks about their upcoming workouts, training schedule, or what they should do on specific days.
        
        The tool shows the next 14 days by default, but you can specify a different number of days if the user requests it.
        
        This tool requires that a fitness plan has already been created.
        """
    ),
    "get_schedule_data": FunctionToolConfig(
        function=get_schedule_data,
        prompt_instructions="""
        Use this tool to get raw schedule data for calendar interfaces or when the system needs access to the ScheduledTrainingDay objects directly.
        
        This returns the actual schedule objects rather than formatted text, making it suitable for programmatic use.
        
        This tool requires that a fitness plan has already been created.
        """
    ),
    "clear_user_session": FunctionToolConfig(
        function=clear_user_session,
        prompt_instructions="""
        Use this tool when the user wants to start completely fresh or clear all their data.
        
        This will clear:
        - User profile information
        - Current fitness plan
        - Workout logs
        - All measurements
        
        Only use this when the user explicitly requests to clear/reset/start over with all their data.
        """
    )
}


def get_tool_functions() -> List[callable]:
    """Get all function tools for the agent."""
    return [config.function for config in FITNESS_TOOLS.values()]


def get_combined_instructions() -> str:
    """Get combined prompt instructions for all tools."""
    instructions = []
    for tool_name, config in FITNESS_TOOLS.items():
        instructions.append(f"## {tool_name.replace('_', ' ').title()}")
        instructions.append(config.prompt_instructions)
        instructions.append("")
    
    return "\n".join(instructions)