github-actions[bot] commited on
Commit
67945d8
Β·
1 Parent(s): a5792ca

πŸ€– Auto-deploy from GitHub (push) - f6b8a83 - 2025-08-05 04:25:29 UTC

Browse files
.env.example CHANGED
@@ -14,10 +14,10 @@ ANTHROPIC_API_KEY=your_anthropic_key_here
14
  # OPENAI_API_KEY=your_openai_api_key_here
15
  # ANTHROPIC_API_KEY=your_anthropic_key_here
16
 
17
- # Optional: Set default model (will use gpt-4o-mini if not set)
 
18
  # AI_MODEL=gpt-4o-mini
19
  # AI_MODEL=claude-3.5-sonnet
20
- # AI_MODEL=gpt-4o
21
 
22
  # Alternative environment variable names (for compatibility)
23
  # ANTHROPIC_MODEL=claude-3.5-haiku
 
14
  # OPENAI_API_KEY=your_openai_api_key_here
15
  # ANTHROPIC_API_KEY=your_anthropic_key_here
16
 
17
+ # Optional: Set default model (will use gpt-4o if not set)
18
+ # AI_MODEL=gpt-4o
19
  # AI_MODEL=gpt-4o-mini
20
  # AI_MODEL=claude-3.5-sonnet
 
21
 
22
  # Alternative environment variable names (for compatibility)
23
  # ANTHROPIC_MODEL=claude-3.5-haiku
apps/gradio-app/src/fitness_gradio/ui/components.py CHANGED
@@ -2,9 +2,10 @@
2
  UI components for the fitness app.
3
  """
4
  import gradio as gr
5
- from typing import List
6
 
7
  from fitness_core.agents import FitnessAgent
 
8
  from .styles import (
9
  HEADER_MARKDOWN,
10
  HELP_CONTENT,
@@ -127,10 +128,13 @@ class UIComponents:
127
  # Add OpenAI models last
128
  dropdown_choices.extend(openai_models)
129
 
 
 
 
130
  # Main model selection dropdown (full width)
131
  model_dropdown = gr.Dropdown(
132
  choices=dropdown_choices,
133
- value="claude-3.5-haiku",
134
  label="Select AI Model",
135
  info="Choose your preferred AI model for fitness guidance",
136
  elem_classes=["model-dropdown"]
@@ -138,7 +142,7 @@ class UIComponents:
138
 
139
  # Hidden component to manage selection (for compatibility)
140
  selected_model = gr.Textbox(
141
- value="claude-3.5-haiku",
142
  visible=False,
143
  label="Selected Model"
144
  )
@@ -318,3 +322,508 @@ class UIComponents:
318
  )
319
 
320
  return plan_display, view_plan_btn, clear_plan_btn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  UI components for the fitness app.
3
  """
4
  import gradio as gr
5
+ from typing import List, Any
6
 
7
  from fitness_core.agents import FitnessAgent
8
+ from fitness_core.agents.providers import ModelProvider
9
  from .styles import (
10
  HEADER_MARKDOWN,
11
  HELP_CONTENT,
 
128
  # Add OpenAI models last
129
  dropdown_choices.extend(openai_models)
130
 
131
+ # Get the configured default model from environment/config
132
+ default_model = ModelProvider.resolve_model_name()
133
+
134
  # Main model selection dropdown (full width)
135
  model_dropdown = gr.Dropdown(
136
  choices=dropdown_choices,
137
+ value=default_model,
138
  label="Select AI Model",
139
  info="Choose your preferred AI model for fitness guidance",
140
  elem_classes=["model-dropdown"]
 
142
 
143
  # Hidden component to manage selection (for compatibility)
144
  selected_model = gr.Textbox(
145
+ value=default_model,
146
  visible=False,
147
  label="Selected Model"
148
  )
 
322
  )
323
 
324
  return plan_display, view_plan_btn, clear_plan_btn
325
+
326
+ @staticmethod
327
+ def format_structured_fitness_plan(plan_obj: Any) -> str:
328
+ """
329
+ Format a structured fitness plan object into a nicely formatted markdown string.
330
+
331
+ Args:
332
+ plan_obj: The fitness plan object with structured attributes
333
+
334
+ Returns:
335
+ Formatted markdown string
336
+ """
337
+ try:
338
+ # Handle structured FitnessPlan object
339
+ if hasattr(plan_obj, 'name') and hasattr(plan_obj, 'training_plan'):
340
+ return UIComponents._format_fitness_plan_object(plan_obj)
341
+
342
+ # Handle string representation of structured object
343
+ elif isinstance(plan_obj, str) and 'training_plan_splits=' in plan_obj:
344
+ return UIComponents._format_structured_plan_string(plan_obj)
345
+
346
+ # Fallback to basic formatting
347
+ else:
348
+ return f"**Fitness Plan**\n\n{str(plan_obj)}"
349
+
350
+ except Exception as e:
351
+ return f"**Error Formatting Plan**\n\nSorry, there was an error formatting your fitness plan: {str(e)}"
352
+
353
+ @staticmethod
354
+ def _format_fitness_plan_object(plan_obj: Any) -> str:
355
+ """Format a structured FitnessPlan object."""
356
+ try:
357
+ # Extract basic plan info
358
+ plan_name = getattr(plan_obj, 'name', 'Fitness Plan')
359
+ plan_goal = getattr(plan_obj, 'goal', '')
360
+ plan_description = getattr(plan_obj, 'description', '')
361
+ meal_plan = getattr(plan_obj, 'meal_plan', '')
362
+
363
+ # Format header
364
+ formatted = f"# πŸ‹οΈ {plan_name}\n\n"
365
+
366
+ if plan_goal:
367
+ formatted += f"**🎯 Goal:** {plan_goal}\n\n"
368
+
369
+ if plan_description:
370
+ formatted += f"**πŸ“‹ Overview:** {plan_description}\n\n"
371
+
372
+ # Format training plan
373
+ training_plan = getattr(plan_obj, 'training_plan', None)
374
+ if training_plan:
375
+ formatted += "## πŸ’ͺ Training Plan\n\n"
376
+ formatted += UIComponents._format_training_plan(training_plan)
377
+
378
+ # Format meal plan
379
+ if meal_plan:
380
+ formatted += "\n## πŸ₯— Meal Plan\n\n"
381
+ formatted += f"{meal_plan}\n\n"
382
+
383
+ # Add footer
384
+ formatted += "## πŸ“Š Additional Information\n\n"
385
+ formatted += "- Plan created with AI assistance\n"
386
+ formatted += "- Customize as needed for your preferences\n"
387
+ formatted += "- Consult healthcare providers for medical advice\n\n"
388
+ formatted += "---\n"
389
+ formatted += "*Your personalized fitness plan is ready! Feel free to ask any questions about the plan or request modifications.*"
390
+
391
+ return formatted
392
+
393
+ except Exception as e:
394
+ return f"**Error Formatting Plan Object**\n\n{str(e)}"
395
+
396
+ @staticmethod
397
+ def _format_training_plan(training_plan: Any) -> str:
398
+ """Format a TrainingPlan object."""
399
+ try:
400
+ formatted = ""
401
+
402
+ # Get training plan details
403
+ plan_name = getattr(training_plan, 'name', 'Training Plan')
404
+ plan_description = getattr(training_plan, 'description', '')
405
+ goal_event = getattr(training_plan, 'goal_event', None)
406
+ target_event_date = getattr(training_plan, 'target_event_date', None)
407
+ total_duration_weeks = getattr(training_plan, 'total_duration_weeks', None)
408
+
409
+ formatted += f"**{plan_name}**\n\n"
410
+ if plan_description:
411
+ formatted += f"{plan_description}\n\n"
412
+
413
+ # Add goal event and timeline information
414
+ if goal_event:
415
+ formatted += f"**🎯 Target Event:** {goal_event}\n"
416
+ if target_event_date:
417
+ formatted += f"**πŸ“… Target Date:** {target_event_date.strftime('%B %d, %Y')}\n"
418
+ if total_duration_weeks:
419
+ formatted += f"**⏱️ Total Duration:** {total_duration_weeks} weeks\n"
420
+
421
+ if goal_event or target_event_date or total_duration_weeks:
422
+ formatted += "\n"
423
+
424
+ # Format training splits with date ranges
425
+ training_splits = getattr(training_plan, 'training_plan_splits', [])
426
+ formatted += UIComponents._format_training_splits_with_dates(training_splits, target_event_date)
427
+
428
+ return formatted
429
+
430
+ except Exception as e:
431
+ return f"Error formatting training plan: {str(e)}"
432
+
433
+ @staticmethod
434
+ def _format_training_splits_with_dates(training_splits: list, target_event_date: Any = None) -> str:
435
+ """Format training splits with calculated date ranges."""
436
+ try:
437
+ from datetime import date, timedelta
438
+
439
+ formatted = ""
440
+ current_date = date.today()
441
+
442
+ # Sort splits by order
443
+ sorted_splits = sorted(training_splits, key=lambda x: getattr(x, 'order', 0))
444
+
445
+ for split in sorted_splits:
446
+ # Get split details
447
+ split_name = getattr(split, 'name', 'Training Split')
448
+ phase_name = getattr(split, 'phase_name', split_name)
449
+ phase_type = getattr(split, 'phase_type', None)
450
+ duration_weeks = getattr(split, 'duration_weeks', 1)
451
+ split_start_date = getattr(split, 'start_date', None)
452
+
453
+ # Calculate start date for this split
454
+ if split_start_date:
455
+ start_date = split_start_date
456
+ else:
457
+ start_date = current_date
458
+
459
+ # Calculate end date
460
+ end_date = start_date + timedelta(weeks=duration_weeks) - timedelta(days=1)
461
+
462
+ # Update current_date for next split
463
+ current_date = end_date + timedelta(days=1)
464
+
465
+ # Format phase type emoji
466
+ phase_emojis = {
467
+ 'base_building': 'πŸ—οΈ',
468
+ 'strength': 'πŸ’ͺ',
469
+ 'power': '⚑',
470
+ 'peak': 'πŸ”₯',
471
+ 'taper': 'πŸ“‰',
472
+ 'recovery': '😌',
473
+ 'maintenance': 'βš–οΈ'
474
+ }
475
+ phase_type_str = str(phase_type).replace('PhaseType.', '').lower() if phase_type else 'training'
476
+ phase_emoji = phase_emojis.get(phase_type_str, 'πŸ“…')
477
+
478
+ # Format the split header with date range
479
+ formatted += f"### {phase_emoji} {phase_name}\n"
480
+ formatted += f"**πŸ“… Duration:** {start_date.strftime('%b %d')} - {end_date.strftime('%b %d, %Y')} ({duration_weeks} week{'s' if duration_weeks != 1 else ''})\n\n"
481
+
482
+ # Add split description and training days
483
+ formatted += UIComponents._format_training_split_content(split)
484
+ formatted += "\n"
485
+
486
+ return formatted
487
+
488
+ except Exception as e:
489
+ return f"Error formatting training splits with dates: {str(e)}"
490
+
491
+ @staticmethod
492
+ def _format_training_split(split: Any) -> str:
493
+ """Format a TrainingPlanSplit object (legacy method, use _format_training_splits_with_dates for new formatting)."""
494
+ try:
495
+ formatted = ""
496
+
497
+ # Get split details
498
+ split_name = getattr(split, 'name', 'Training Split')
499
+ split_description = getattr(split, 'description', '')
500
+
501
+ formatted += f"### πŸ“… {split_name}\n\n"
502
+ if split_description:
503
+ formatted += f"{split_description}\n\n"
504
+
505
+ # Format training days
506
+ training_days = getattr(split, 'training_days', [])
507
+ for day in training_days:
508
+ formatted += UIComponents._format_training_day(day)
509
+ formatted += "\n"
510
+
511
+ return formatted
512
+
513
+ except Exception as e:
514
+ return f"Error formatting training split: {str(e)}"
515
+
516
+ @staticmethod
517
+ def _format_training_split_content(split: Any) -> str:
518
+ """Format the content of a TrainingPlanSplit object (without header/dates)."""
519
+ try:
520
+ formatted = ""
521
+
522
+ # Get split details
523
+ split_description = getattr(split, 'description', '')
524
+
525
+ if split_description:
526
+ formatted += f"*{split_description}*\n\n"
527
+
528
+ # Format training days
529
+ training_days = getattr(split, 'training_days', [])
530
+ for day in training_days:
531
+ formatted += UIComponents._format_training_day(day)
532
+ formatted += "\n"
533
+
534
+ return formatted
535
+
536
+ except Exception as e:
537
+ return f"Error formatting training split content: {str(e)}"
538
+
539
+ @staticmethod
540
+ def _format_training_day(day: Any) -> str:
541
+ """Format a TrainingDay object."""
542
+ try:
543
+ formatted = ""
544
+
545
+ # Get day details
546
+ day_name = getattr(day, 'name', 'Training Day')
547
+ day_description = getattr(day, 'description', '')
548
+ day_order = getattr(day, 'order_number', '')
549
+ is_rest_day = getattr(day, 'rest_day', False)
550
+ day_intensity = getattr(day, 'intensity', None)
551
+
552
+ # Format day header
553
+ day_emoji = "😴" if is_rest_day else "πŸ’ͺ"
554
+ formatted += f"#### {day_emoji} Day {day_order}: {day_name}\n\n"
555
+
556
+ if day_description:
557
+ formatted += f"*{day_description}*\n\n"
558
+
559
+ if day_intensity and not is_rest_day:
560
+ intensity_emojis = {
561
+ 'light': '🟒',
562
+ 'moderate': '🟑',
563
+ 'heavy': 'πŸ”΄',
564
+ 'max_effort': 'πŸ”₯'
565
+ }
566
+ intensity_str = str(day_intensity).replace('IntensityLevel.', '').replace('<', '').replace('>', '').split(':')[0]
567
+ intensity_emoji = intensity_emojis.get(intensity_str.lower(), 'βšͺ')
568
+ formatted += f"**Intensity:** {intensity_emoji} {intensity_str.title()}\n\n"
569
+
570
+ # Format exercises
571
+ if not is_rest_day:
572
+ exercises = getattr(day, 'exercises', [])
573
+ if exercises:
574
+ formatted += "**Exercises:**\n\n"
575
+ for i, exercise in enumerate(exercises, 1):
576
+ formatted += UIComponents._format_exercise(exercise, i)
577
+ formatted += "\n"
578
+ else:
579
+ formatted += "**Rest Day** - Focus on recovery, light stretching, or gentle activities.\n\n"
580
+
581
+ return formatted
582
+
583
+ except Exception as e:
584
+ return f"Error formatting training day: {str(e)}"
585
+
586
+ @staticmethod
587
+ def _format_exercise(exercise: Any, number: int) -> str:
588
+ """Format an Exercise object."""
589
+ try:
590
+ # Get exercise details
591
+ name = getattr(exercise, 'name', 'Exercise')
592
+ description = getattr(exercise, 'description', '')
593
+ sets = getattr(exercise, 'sets', None)
594
+ reps = getattr(exercise, 'reps', None)
595
+ duration = getattr(exercise, 'duration', None)
596
+ intensity = getattr(exercise, 'intensity', None)
597
+
598
+ formatted = f"{number}. **{name}**\n"
599
+
600
+ # Add sets/reps/duration info
601
+ workout_details = []
602
+ if sets:
603
+ workout_details.append(f"{sets} sets")
604
+ if reps:
605
+ workout_details.append(f"{reps} reps")
606
+ if duration:
607
+ workout_details.append(f"{duration}s")
608
+
609
+ if workout_details:
610
+ formatted += f" *{' Γ— '.join(workout_details)}*"
611
+
612
+ if intensity:
613
+ intensity_emojis = {
614
+ 'light': '🟒',
615
+ 'moderate': '🟑',
616
+ 'heavy': 'πŸ”΄',
617
+ 'max_effort': 'πŸ”₯'
618
+ }
619
+ intensity_str = str(intensity).replace('IntensityLevel.', '').replace('<', '').replace('>', '').split(':')[0]
620
+ intensity_emoji = intensity_emojis.get(intensity_str.lower(), 'βšͺ')
621
+ formatted += f" - {intensity_emoji} {intensity_str.title()}"
622
+
623
+ formatted += "\n"
624
+
625
+ if description:
626
+ formatted += f" *{description}*\n"
627
+
628
+ return formatted
629
+
630
+ except Exception as e:
631
+ return f"Error formatting exercise: {str(e)}"
632
+
633
+ @staticmethod
634
+ def _format_structured_plan_string(plan_str: str) -> str:
635
+ """Format a string representation of a structured fitness plan."""
636
+ try:
637
+ import re
638
+
639
+ # Extract plan name
640
+ name_match = re.search(r"πŸ‹οΈ\s*([^\n]*?)Training Plan", plan_str)
641
+ if not name_match:
642
+ name_match = re.search(r"name='([^']*)'", plan_str)
643
+ plan_name = name_match.group(1).strip() if name_match else "Fitness Plan"
644
+
645
+ # Extract meal plan section
646
+ meal_match = re.search(r"πŸ₯— Meal Plan\n(.*?)(?=πŸ“Š|$)", plan_str, re.DOTALL)
647
+ meal_plan = meal_match.group(1).strip() if meal_match else ""
648
+
649
+ # Extract training plan section with structured data
650
+ formatted = f"# πŸ‹οΈ {plan_name}\n\n"
651
+
652
+ # Parse and format the structured training data
653
+ training_match = re.search(r"training_plan_splits=\[(.*?)\]\]", plan_str, re.DOTALL)
654
+ if training_match:
655
+ formatted += "## πŸ’ͺ Training Plan\n\n"
656
+ formatted += UIComponents._parse_and_format_training_data(training_match.group(1))
657
+
658
+ # Add meal plan
659
+ if meal_plan:
660
+ formatted += "\n## πŸ₯— Meal Plan\n\n"
661
+ formatted += f"{meal_plan}\n\n"
662
+
663
+ # Add footer
664
+ formatted += "## πŸ“Š Additional Information\n\n"
665
+ formatted += "- Plan created with AI assistance\n"
666
+ formatted += "- Customize as needed for your preferences\n"
667
+ formatted += "- Consult healthcare providers for medical advice\n\n"
668
+ formatted += "---\n"
669
+ formatted += "*Your personalized fitness plan is ready! Feel free to ask any questions about the plan or request modifications.*"
670
+
671
+ return formatted
672
+
673
+ except Exception as e:
674
+ return f"**Error Parsing Structured Plan**\n\n{str(e)}\n\nRaw content:\n{plan_str}"
675
+
676
+ @staticmethod
677
+ def _parse_and_format_training_data(training_data: str) -> str:
678
+ """Parse and format the training data from string representation."""
679
+ try:
680
+ import re
681
+
682
+ formatted = ""
683
+
684
+ # Extract split information
685
+ split_name_match = re.search(r"name='([^']*)'", training_data)
686
+ split_name = split_name_match.group(1) if split_name_match else "Weekly Split"
687
+
688
+ split_desc_match = re.search(r"description='([^']*)'", training_data)
689
+ split_desc = split_desc_match.group(1) if split_desc_match else ""
690
+
691
+ formatted += f"**{split_name}**\n\n"
692
+ if split_desc:
693
+ formatted += f"{split_desc}\n\n"
694
+
695
+ # Extract training days
696
+ days_pattern = r"TrainingDay\((.*?)\)(?=, TrainingDay\(|$)"
697
+ days = re.findall(days_pattern, training_data, re.DOTALL)
698
+
699
+ for day_data in days:
700
+ formatted += UIComponents._parse_and_format_day_data(day_data)
701
+ formatted += "\n"
702
+
703
+ return formatted
704
+
705
+ except Exception as e:
706
+ return f"Error parsing training data: {str(e)}"
707
+
708
+ @staticmethod
709
+ def _parse_and_format_day_data(day_data: str) -> str:
710
+ """Parse and format a single training day from string representation."""
711
+ try:
712
+ import re
713
+
714
+ # Extract day details
715
+ name_match = re.search(r"name='([^']*)'", day_data)
716
+ day_name = name_match.group(1) if name_match else "Training Day"
717
+
718
+ order_match = re.search(r"order_number=(\d+)", day_data)
719
+ day_order = order_match.group(1) if order_match else "1"
720
+
721
+ desc_match = re.search(r"description='([^']*)'", day_data)
722
+ day_description = desc_match.group(1) if desc_match else ""
723
+
724
+ rest_match = re.search(r"rest_day=(\w+)", day_data)
725
+ is_rest_day = rest_match and rest_match.group(1) == "True"
726
+
727
+ intensity_match = re.search(r"intensity=<IntensityLevel\.(\w+):", day_data)
728
+ day_intensity = intensity_match.group(1) if intensity_match else None
729
+
730
+ # Format day header
731
+ day_emoji = "😴" if is_rest_day else "πŸ’ͺ"
732
+ formatted = f"#### {day_emoji} Day {day_order}: {day_name}\n\n"
733
+
734
+ if day_description:
735
+ formatted += f"*{day_description}*\n\n"
736
+
737
+ if day_intensity and not is_rest_day:
738
+ intensity_emojis = {
739
+ 'LIGHT': '🟒',
740
+ 'MODERATE': '🟑',
741
+ 'HEAVY': 'πŸ”΄',
742
+ 'MAX_EFFORT': 'πŸ”₯'
743
+ }
744
+ intensity_emoji = intensity_emojis.get(day_intensity, 'βšͺ')
745
+ formatted += f"**Intensity:** {intensity_emoji} {day_intensity.title()}\n\n"
746
+
747
+ # Parse exercises if not rest day
748
+ if not is_rest_day and 'exercises=[' in day_data:
749
+ exercises_match = re.search(r"exercises=\[(.*?)\]", day_data, re.DOTALL)
750
+ if exercises_match:
751
+ exercises_data = exercises_match.group(1)
752
+ formatted += "**Exercises:**\n\n"
753
+ formatted += UIComponents._parse_and_format_exercises(exercises_data)
754
+ elif is_rest_day:
755
+ formatted += "**Rest Day** - Focus on recovery, light stretching, or gentle activities.\n\n"
756
+
757
+ return formatted
758
+
759
+ except Exception as e:
760
+ return f"Error parsing day data: {str(e)}"
761
+
762
+ @staticmethod
763
+ def _parse_and_format_exercises(exercises_data: str) -> str:
764
+ """Parse and format exercises from string representation."""
765
+ try:
766
+ import re
767
+
768
+ formatted = ""
769
+
770
+ # Extract individual exercises
771
+ exercise_pattern = r"Exercise\((.*?)\)(?=, Exercise\(|$)"
772
+ exercises = re.findall(exercise_pattern, exercises_data, re.DOTALL)
773
+
774
+ for i, exercise_data in enumerate(exercises, 1):
775
+ # Extract exercise details
776
+ name_match = re.search(r"name='([^']*)'", exercise_data)
777
+ name = name_match.group(1) if name_match else "Exercise"
778
+
779
+ desc_match = re.search(r"description='([^']*)'", exercise_data)
780
+ description = desc_match.group(1) if desc_match else ""
781
+
782
+ sets_match = re.search(r"sets=(\d+)", exercise_data)
783
+ sets = sets_match.group(1) if sets_match else None
784
+
785
+ reps_match = re.search(r"reps=(\d+)", exercise_data)
786
+ reps = reps_match.group(1) if reps_match else None
787
+
788
+ duration_match = re.search(r"duration=(\d+)", exercise_data)
789
+ duration = duration_match.group(1) if duration_match else None
790
+
791
+ intensity_match = re.search(r"intensity=<IntensityLevel\.(\w+):", exercise_data)
792
+ intensity = intensity_match.group(1) if intensity_match else None
793
+
794
+ # Format exercise
795
+ formatted += f"{i}. **{name}**\n"
796
+
797
+ # Add workout details
798
+ workout_details = []
799
+ if sets:
800
+ workout_details.append(f"{sets} sets")
801
+ if reps:
802
+ workout_details.append(f"{reps} reps")
803
+ if duration:
804
+ workout_details.append(f"{duration}s")
805
+
806
+ if workout_details:
807
+ formatted += f" *{' Γ— '.join(workout_details)}*"
808
+
809
+ if intensity:
810
+ intensity_emojis = {
811
+ 'LIGHT': '🟒',
812
+ 'MODERATE': '🟑',
813
+ 'HEAVY': 'πŸ”΄',
814
+ 'MAX_EFFORT': 'πŸ”₯'
815
+ }
816
+ intensity_emoji = intensity_emojis.get(intensity, 'βšͺ')
817
+ formatted += f" - {intensity_emoji} {intensity.title()}"
818
+
819
+ formatted += "\n"
820
+
821
+ if description:
822
+ formatted += f" *{description}*\n"
823
+
824
+ formatted += "\n"
825
+
826
+ return formatted
827
+
828
+ except Exception as e:
829
+ return f"Error parsing exercises: {str(e)}"
apps/gradio-app/src/fitness_gradio/ui/handlers.py CHANGED
@@ -7,6 +7,7 @@ import os
7
  from typing import List, Dict, Union, Generator, Any, Tuple, Optional
8
 
9
  from fitness_core.agents import FitnessAgent
 
10
  from fitness_core.services import ConversationManager, FitnessAgentRunner, ResponseFormatter
11
  from fitness_core.utils import get_logger
12
  from .tts_utils import generate_speech_for_text, generate_speech_for_session, clean_tts_markup
@@ -24,7 +25,7 @@ logger = get_logger(__name__)
24
  # Global state management
25
  conversation_manager = ConversationManager()
26
  current_agent = None
27
- current_model = "llama-3.3-70b-versatile"
28
 
29
 
30
  class UIHandlers:
 
7
  from typing import List, Dict, Union, Generator, Any, Tuple, Optional
8
 
9
  from fitness_core.agents import FitnessAgent
10
+ from fitness_core.agents.providers import ModelProvider
11
  from fitness_core.services import ConversationManager, FitnessAgentRunner, ResponseFormatter
12
  from fitness_core.utils import get_logger
13
  from .tts_utils import generate_speech_for_text, generate_speech_for_session, clean_tts_markup
 
25
  # Global state management
26
  conversation_manager = ConversationManager()
27
  current_agent = None
28
+ current_model = ModelProvider.resolve_model_name() # Use configured default model
29
 
30
 
31
  class UIHandlers:
apps/gradio-app/src/fitness_gradio/ui/styles.py CHANGED
@@ -339,7 +339,7 @@ MODEL_COMPARISON_CONTENT = """
339
  - llama-3.3-70b-versatile, mixtral-8x7b-32768
340
 
341
  **βœ… GOOD for Fitness Plans (may need more specific prompting):**
342
- - claude-3.5-haiku, gpt-4o-mini, gpt-3.5-turbo
343
  - llama3-70b-8192, qwen3-32b, kimi-k2-instruct
344
 
345
  **⚠️ LIMITED for Complex Plans (basic guidance only):**
@@ -350,11 +350,11 @@ MODEL_COMPARISON_CONTENT = """
350
  - gemma-7b-it (may produce incomplete or empty plans)
351
 
352
  ### 🎯 Recommendations by Use Case
353
- - **Quick questions**: claude-3.5-haiku, gpt-4o-mini, gpt-3.5-turbo, any Groq model
354
- - **Comprehensive fitness plans**: claude-3.5-sonnet+, gpt-4o+, llama-3.3-70b-versatile, mixtral-8x7b-32768
355
- - **Complex analysis**: claude-4-opus, gpt-4o, o1-preview
356
- - **Budget-conscious**: claude-3-haiku, gpt-3.5-turbo, gpt-4o-mini
357
- - **Speed priority**: Any Groq model, claude-3.5-haiku, gpt-4o-mini
358
 
359
  ### πŸ”§ Troubleshooting Complex Tasks
360
  If you experience **empty or incomplete fitness plans** with smaller models:
 
339
  - llama-3.3-70b-versatile, mixtral-8x7b-32768
340
 
341
  **βœ… GOOD for Fitness Plans (may need more specific prompting):**
342
+ - gpt-4o, claude-3.5-haiku, gpt-4o-mini, gpt-3.5-turbo
343
  - llama3-70b-8192, qwen3-32b, kimi-k2-instruct
344
 
345
  **⚠️ LIMITED for Complex Plans (basic guidance only):**
 
350
  - gemma-7b-it (may produce incomplete or empty plans)
351
 
352
  ### 🎯 Recommendations by Use Case
353
+ - **Quick questions**: gpt-4o, claude-3.5-haiku, gpt-4o-mini, gpt-3.5-turbo, any Groq model
354
+ - **Comprehensive fitness plans**: gpt-4o+, claude-3.5-sonnet+, llama-3.3-70b-versatile, mixtral-8x7b-32768
355
+ - **Complex analysis**: gpt-4o, claude-4-opus, o1-preview
356
+ - **Budget-conscious**: gpt-4o-mini, claude-3-haiku, gpt-3.5-turbo
357
+ - **Speed priority**: Any Groq model, gpt-4o-mini, claude-3.5-haiku
358
 
359
  ### πŸ”§ Troubleshooting Complex Tasks
360
  If you experience **empty or incomplete fitness plans** with smaller models:
apps/gradio-app/src/fitness_gradio/ui/voice_conversation.py CHANGED
@@ -19,6 +19,7 @@ except ImportError:
19
 
20
  from fitness_core.utils import get_logger
21
  from fitness_core.agents import FitnessAgent
 
22
  from fitness_core.services import FitnessAgentRunner, ConversationManager
23
  from fitness_core.services.formatters import ResponseFormatter
24
  from .tts_utils import generate_speech_for_session, clean_tts_markup
@@ -181,7 +182,7 @@ class VoiceConversationManager:
181
  """
182
  try:
183
  # Create a fitness agent for this conversation
184
- agent = FitnessAgent(model_name or "llama-3.3-70b-versatile")
185
 
186
  # Get input for agent from conversation manager (same as text chat)
187
  agent_input = conversation_manager.get_input_for_agent()
 
19
 
20
  from fitness_core.utils import get_logger
21
  from fitness_core.agents import FitnessAgent
22
+ from fitness_core.agents.providers import ModelProvider
23
  from fitness_core.services import FitnessAgentRunner, ConversationManager
24
  from fitness_core.services.formatters import ResponseFormatter
25
  from .tts_utils import generate_speech_for_session, clean_tts_markup
 
182
  """
183
  try:
184
  # Create a fitness agent for this conversation
185
+ agent = FitnessAgent(model_name or ModelProvider.resolve_model_name())
186
 
187
  # Get input for agent from conversation manager (same as text chat)
188
  agent_input = conversation_manager.get_input_for_agent()
shared/src/fitness_core/agents/providers.py CHANGED
@@ -114,8 +114,15 @@ class ModelProvider:
114
  def get_recommended_models(cls) -> List[str]:
115
  """Get a list of recommended models that are most likely to be available."""
116
  return [
117
- # Groq recommendations (fast and cost-effective) - now default
118
- "llama-3.3-70b-versatile", # Latest and most capable Llama model - DEFAULT
 
 
 
 
 
 
 
119
  "llama-3.1-8b-instant", # Fastest for simple tasks
120
  "gemma2-9b-it", # Efficient Google model
121
  "mixtral-8x7b-32768", # Excellent reasoning with large context
@@ -126,13 +133,6 @@ class ModelProvider:
126
  "claude-3.5-sonnet", # Stable version, widely available
127
  "claude-3.5-sonnet-latest", # Latest improvements
128
  "claude-3.7-sonnet", # Newest stable with extended thinking
129
-
130
- # OpenAI recommendations
131
- "gpt-4o-mini", # Best balance of capability and cost
132
- "gpt-4o", # Latest flagship model
133
- "gpt-3.5-turbo", # Most cost-effective OpenAI model
134
- "gpt-4-turbo", # Solid previous generation
135
- "o1-mini", # Good reasoning capabilities
136
  ]
137
 
138
  @classmethod
@@ -363,15 +363,15 @@ class ModelProvider:
363
  os.getenv("ANTHROPIC_MODEL") or
364
  os.getenv("OPENAI_MODEL") or
365
  os.getenv("GROQ_MODEL") or
366
- "llama-3.3-70b-versatile" # Default fallback - latest Groq Llama model for excellent performance
367
  )
368
 
369
  # Validate the model name
370
  is_valid, validation_message = cls.validate_model_name(model_name)
371
  if not is_valid:
372
  print(f"Warning: {validation_message}")
373
- print(f"Falling back to default model: llama-3.3-70b-versatile")
374
- model_name = "llama-3.3-70b-versatile"
375
 
376
  return model_name
377
 
 
114
  def get_recommended_models(cls) -> List[str]:
115
  """Get a list of recommended models that are most likely to be available."""
116
  return [
117
+ # OpenAI recommendations (now default)
118
+ "gpt-4o", # Latest flagship model - NEW DEFAULT
119
+ "gpt-4o-mini", # Best balance of capability and cost
120
+ "gpt-3.5-turbo", # Most cost-effective OpenAI model
121
+ "gpt-4-turbo", # Solid previous generation
122
+ "o1-mini", # Good reasoning capabilities
123
+
124
+ # Groq recommendations (fast and cost-effective)
125
+ "llama-3.3-70b-versatile", # Latest and most capable Llama model
126
  "llama-3.1-8b-instant", # Fastest for simple tasks
127
  "gemma2-9b-it", # Efficient Google model
128
  "mixtral-8x7b-32768", # Excellent reasoning with large context
 
133
  "claude-3.5-sonnet", # Stable version, widely available
134
  "claude-3.5-sonnet-latest", # Latest improvements
135
  "claude-3.7-sonnet", # Newest stable with extended thinking
 
 
 
 
 
 
 
136
  ]
137
 
138
  @classmethod
 
363
  os.getenv("ANTHROPIC_MODEL") or
364
  os.getenv("OPENAI_MODEL") or
365
  os.getenv("GROQ_MODEL") or
366
+ "gpt-4o" # Default fallback - OpenAI's flagship model for excellent performance
367
  )
368
 
369
  # Validate the model name
370
  is_valid, validation_message = cls.validate_model_name(model_name)
371
  if not is_valid:
372
  print(f"Warning: {validation_message}")
373
+ print(f"Falling back to default model: gpt-4o")
374
+ model_name = "gpt-4o"
375
 
376
  return model_name
377
 
shared/src/fitness_core/agents/structured_output_models.py CHANGED
@@ -9,6 +9,7 @@ from enum import Enum
9
 
10
  class IntensityLevel(str, Enum):
11
  """Intensity levels for exercises."""
 
12
  LIGHT = "light"
13
  MODERATE = "moderate"
14
  HEAVY = "heavy"
@@ -20,14 +21,17 @@ class Exercise(BaseModel):
20
  name: str = Field(
21
  description="The name of the exercise (e.g., 'Push-ups', 'Squats', 'Deadlift')"
22
  )
23
- description: Optional[str] = Field(
24
- default=None,
25
  description="Brief description of how to perform the exercise, including proper form and technique"
26
  )
27
  duration: Optional[int] = Field(
28
  default=None,
29
  description="Duration of the exercise in seconds (for time-based exercises like planks or cardio)"
30
  )
 
 
 
 
31
  sets: Optional[int] = Field(
32
  default=None,
33
  description="Number of sets to perform for this exercise (e.g., 3 sets). Used for strength training and resistance exercises."
@@ -36,7 +40,7 @@ class Exercise(BaseModel):
36
  default=None,
37
  description="Number of repetitions per set (e.g., 10 reps). Used for strength training exercises. Can be a range like '8-12' for variety."
38
  )
39
- intensity: Optional[IntensityLevel] = Field(
40
  default=None,
41
  description="Intensity level of the exercise. 'light' for warm-up/mobility work, 'moderate' for general training, 'heavy' for strength focus (80-90% effort), 'max_effort' for testing/competition lifts. Can be left blank for rest days or exercises where intensity is not applicable."
42
  )
@@ -53,63 +57,65 @@ class TrainingDay(BaseModel):
53
  description: str = Field(
54
  description="Brief overview of the day's focus and training objectives (e.g., 'Focus on compound upper body movements with moderate intensity')"
55
  )
56
- exercises: Optional[List[Exercise]] = Field( # Made optional
57
- default=None,
58
- description="List of exercises to be performed on this training day, with full details including sets, reps, and intensity. Leave empty/null for complete rest days."
59
  )
60
- intensity: Optional[IntensityLevel] = Field(
61
- default=None,
62
  description="Overall intensity of the training day. Use 'light' for recovery/mobility, 'moderate' for standard training, 'heavy' for strength focus, 'max_effort' for testing. Leave blank for complete rest days."
63
  )
64
- rest_day: bool = Field(
65
- default=False,
66
- description="True if this is a complete rest day with no exercises"
67
- )
68
-
69
 
70
- class TrainingPlanSplit(BaseModel):
71
- """A training split represents a complete cycle of training days (e.g., a weekly routine)."""
72
  name: str = Field(
73
  description="Descriptive name for the split approach (e.g., 'Push/Pull/Legs Split', 'Upper/Lower Split', 'Full Body Circuit')"
74
  )
75
- order: int = Field(
76
- description="Sequential order when multiple splits are used in periodization (1 for first phase, 2 for second phase, etc.)"
77
- )
78
  description: str = Field(
79
- description="Detailed explanation of the split's training philosophy, target muscle groups, and how days are organized"
80
- )
81
- start_date: Optional[date] = Field(
82
- default=None,
83
- description="Optional start date for this specific split. If not provided, will be calculated based on previous splits. Useful for precise scheduling of periodized programs."
84
  )
85
  training_days: List[TrainingDay] = Field(
86
- description="All training days in the split, ordered sequentially. Include rest days for complete weekly schedules."
87
  )
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  class TrainingPlan(BaseModel):
91
- """Complete training plan containing one or more training splits."""
92
  name: str = Field(
93
- description="Catchy name that reflects the plan's focus (e.g., 'Beginner Strength Foundation', 'Advanced Powerlifting Prep')"
94
  )
95
  description: str = Field(
96
- description="Comprehensive overview including training philosophy, progression strategy, target audience, and expected timeline"
97
  )
98
- training_plan_splits: List[TrainingPlanSplit] = Field(
99
- description="Ordered list of training splits. Single split for consistent routines, multiple splits for periodized programs with distinct phases (base building β†’ strength β†’ peaking)"
100
  )
101
 
102
 
103
  class FitnessPlan(BaseModel):
104
  """Structured fitness plan model for LLM output."""
105
  name: str = Field(
106
- description="Catchy, descriptive name for the fitness plan (e.g., 'Beginner Strength Builder', '30-Day Fat Loss Challenge')"
107
  )
108
  goal: str = Field(
109
- description="Primary goal of the fitness plan (e.g., 'Build muscle', 'Lose weight', 'Improve endurance'). Should be specific and measurable."
110
  )
111
  description: str = Field(
112
- description="Comprehensive overview of the fitness plan, including goals, target audience, and expected outcomes"
113
  )
114
  training_plan: TrainingPlan = Field(
115
  description="The training plan includes all of the workout splits, training days, and exercises"
@@ -117,11 +123,10 @@ class FitnessPlan(BaseModel):
117
  meal_plan: str = Field(
118
  description="Detailed nutrition guidance including meal suggestions, macronutrient targets, and eating schedule. Should be practical and specific to the fitness goals."
119
  )
120
- start_date: Optional[date] = Field(
121
  default_factory=lambda: date.today(),
122
  description="The date when the user should start this fitness plan"
123
  )
124
- target_date: Optional[date] = Field(
125
- default=None,
126
- description="Optional target completion date or milestone date for the fitness plan"
127
  )
 
9
 
10
  class IntensityLevel(str, Enum):
11
  """Intensity levels for exercises."""
12
+ REST = "rest"
13
  LIGHT = "light"
14
  MODERATE = "moderate"
15
  HEAVY = "heavy"
 
21
  name: str = Field(
22
  description="The name of the exercise (e.g., 'Push-ups', 'Squats', 'Deadlift')"
23
  )
24
+ description: str = Field(
 
25
  description="Brief description of how to perform the exercise, including proper form and technique"
26
  )
27
  duration: Optional[int] = Field(
28
  default=None,
29
  description="Duration of the exercise in seconds (for time-based exercises like planks or cardio)"
30
  )
31
+ distance: Optional[float] = Field(
32
+ default=None,
33
+ description="Distance to be covered in meters (for running, cycling, etc.)"
34
+ )
35
  sets: Optional[int] = Field(
36
  default=None,
37
  description="Number of sets to perform for this exercise (e.g., 3 sets). Used for strength training and resistance exercises."
 
40
  default=None,
41
  description="Number of repetitions per set (e.g., 10 reps). Used for strength training exercises. Can be a range like '8-12' for variety."
42
  )
43
+ intensity: IntensityLevel = Field(
44
  default=None,
45
  description="Intensity level of the exercise. 'light' for warm-up/mobility work, 'moderate' for general training, 'heavy' for strength focus (80-90% effort), 'max_effort' for testing/competition lifts. Can be left blank for rest days or exercises where intensity is not applicable."
46
  )
 
57
  description: str = Field(
58
  description="Brief overview of the day's focus and training objectives (e.g., 'Focus on compound upper body movements with moderate intensity')"
59
  )
60
+ exercises: List[Exercise] = Field(
61
+ description="List of exercises to be performed on this training day, with full details including sets, reps, and intensity. Include good rest activities for complete rest days."
 
62
  )
63
+ intensity: IntensityLevel = Field(
 
64
  description="Overall intensity of the training day. Use 'light' for recovery/mobility, 'moderate' for standard training, 'heavy' for strength focus, 'max_effort' for testing. Leave blank for complete rest days."
65
  )
 
 
 
 
 
66
 
67
+ class TrainingSplit(BaseModel):
68
+ """A training split represents a complete cycle of training days within a specific periodization phase."""
69
  name: str = Field(
70
  description="Descriptive name for the split approach (e.g., 'Push/Pull/Legs Split', 'Upper/Lower Split', 'Full Body Circuit')"
71
  )
 
 
 
72
  description: str = Field(
73
+ description="Detailed explanation of the split's training philosophy, target muscle groups, how days are organized, and how this phase fits into the overall periodization strategy"
 
 
 
 
74
  )
75
  training_days: List[TrainingDay] = Field(
76
+ description="All training days in the training split, ordered sequentially. Make sure to include all the rest days needed in the split."
77
  )
78
 
79
+ class TrainingPeriod(BaseModel):
80
+ name: str = Field(
81
+ description="Descriptive name for the period (e.g., 'Base Phase', 'Advanced Phase', 'Preparation Phase')"
82
+ )
83
+ description: str = Field(
84
+ description="Overview of the period's focus, training goals, and how it fits into the overall plan"
85
+ )
86
+ start_date: date = Field(
87
+ description="Start date for this specific training period."
88
+ )
89
+ intensity: IntensityLevel = Field(
90
+ description="Overall intensity level for this period. Use 'light' for recovery, 'moderate' for general training, 'heavy' for strength focus, 'max_effort' for testing, 'rest' for complete rest."
91
+ )
92
+ training_split: TrainingSplit = Field(
93
+ description="The training split that will be followed during this period. Should include all training days and rest days as the schedule will be created assuming a rolling schedule of training splits."
94
+ )
95
 
96
  class TrainingPlan(BaseModel):
97
+ """Complete periodized training plan with progressive phases leading to a target goal/event."""
98
  name: str = Field(
99
+ description="Name that reflects the plan's focus and target event"
100
  )
101
  description: str = Field(
102
+ description="Comprehensive overview including training philosophy, periodization strategy, target audience, expected timeline, and how the phases progress toward the goal"
103
  )
104
+ training_periods: List[TrainingPeriod] = Field(
105
+ description="Ordered list of TrainingPeriod objects representing different phases of the training plan."
106
  )
107
 
108
 
109
  class FitnessPlan(BaseModel):
110
  """Structured fitness plan model for LLM output."""
111
  name: str = Field(
112
+ description="Name for the fitness plan"
113
  )
114
  goal: str = Field(
115
+ description="Primary goal of the fitness plan"
116
  )
117
  description: str = Field(
118
+ description="Comprehensive overview of the fitness plan, including a brief overview of how the training plan and meal plan meet the goal."
119
  )
120
  training_plan: TrainingPlan = Field(
121
  description="The training plan includes all of the workout splits, training days, and exercises"
 
123
  meal_plan: str = Field(
124
  description="Detailed nutrition guidance including meal suggestions, macronutrient targets, and eating schedule. Should be practical and specific to the fitness goals."
125
  )
126
+ start_date: date = Field(
127
  default_factory=lambda: date.today(),
128
  description="The date when the user should start this fitness plan"
129
  )
130
+ target_date: date = Field(
131
+ description="The target completion date or milestone date for the fitness plan"
 
132
  )
shared/src/fitness_core/agents/tools.py CHANGED
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
  from datetime import date, timedelta
7
  from agents import function_tool, RunContextWrapper
8
 
9
- from .structured_output_models import FitnessPlan, TrainingDay
10
 
11
 
12
  @dataclass
@@ -46,44 +46,44 @@ def build_fitness_schedule(fitness_plan: FitnessPlan, start_date: Optional[date]
46
  schedule = []
47
  current_date = start_date
48
 
49
- # Sort splits by order to ensure proper sequencing
50
- sorted_splits = sorted(fitness_plan.training_plan.training_plan_splits, key=lambda x: x.order)
51
-
52
- for split in sorted_splits:
53
- # Use split's start_date if provided, otherwise use current_date
54
- split_start_date = split.start_date if split.start_date else current_date
55
- split_current_date = split_start_date
 
56
 
57
  # Calculate how many days this split's cycle is
58
  week_number = 1
59
 
60
- # Continue cycling through the split until we reach the end date or run out of splits
61
- while (end_date is None or split_current_date <= end_date):
62
- for day_idx, training_day in enumerate(split.training_days):
63
  # Stop if we've reached the end date
64
- if end_date and split_current_date > end_date:
65
  break
66
 
67
  scheduled_day = ScheduledTrainingDay(
68
- date=split_current_date,
69
  training_day=training_day,
70
- split_name=split.name,
71
  week_number=week_number,
72
  day_in_week=day_idx + 1
73
  )
74
  schedule.append(scheduled_day)
75
- split_current_date += timedelta(days=1)
76
 
77
  # Increment week number after completing a full cycle
78
  week_number += 1
79
 
80
- # If this is the last split and we don't have an end date, break after one cycle
81
- # to avoid infinite loops
82
- if end_date is None and split == sorted_splits[-1]:
83
- break
84
 
85
- # Update current_date for the next split (if no explicit start_date is set for next split)
86
- current_date = split_current_date
87
 
88
  return schedule
89
 
@@ -123,7 +123,12 @@ def format_schedule_summary(schedule: List[ScheduledTrainingDay], days_to_show:
123
  day_str = scheduled_day.date.strftime("%a, %b %d")
124
  intensity_str = f" ({scheduled_day.training_day.intensity.value})" if scheduled_day.training_day.intensity else ""
125
 
126
- if scheduled_day.training_day.rest_day:
 
 
 
 
 
127
  summary_lines.append(f"β€’ {day_str}: {scheduled_day.training_day.name}")
128
  else:
129
  exercise_count = len(scheduled_day.training_day.exercises) if scheduled_day.training_day.exercises else 0
@@ -216,16 +221,42 @@ FITNESS_TOOLS = {
216
  function=create_fitness_plan,
217
  prompt_instructions="""
218
  When the user requests a fitness plan, use the create_fitness_plan tool with a fully completed FitnessPlan object.
219
-
 
 
 
 
220
  The FitnessPlan object must include:
221
  - name: A descriptive name for the fitness plan
222
- - goal: The primary fitness goal
223
- - training_plan: Detailed training/workout information with splits and days
224
- - meal_plan: Comprehensive nutrition and meal planning details
 
 
 
 
 
 
 
 
 
 
225
  - start_date: When the plan should begin (defaults to today)
226
- - target_date: Optional end date for the plan
227
 
228
- This tool automatically builds a date-based schedule and saves the plan to the FitnessAgent class.
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  Do not read the plan back to the user in the conversation. The user can already see it in the UI component.
231
 
 
6
  from datetime import date, timedelta
7
  from agents import function_tool, RunContextWrapper
8
 
9
+ from .structured_output_models import FitnessPlan, TrainingDay, IntensityLevel
10
 
11
 
12
  @dataclass
 
46
  schedule = []
47
  current_date = start_date
48
 
49
+ # Process training periods in order
50
+ for period in fitness_plan.training_plan.training_periods:
51
+ # Use period's start_date if provided, otherwise use current_date
52
+ period_start_date = period.start_date if period.start_date else current_date
53
+ period_current_date = period_start_date
54
+
55
+ # Get the training split for this period
56
+ training_split = period.training_split
57
 
58
  # Calculate how many days this split's cycle is
59
  week_number = 1
60
 
61
+ # Continue cycling through the split until we reach the end date or move to next period
62
+ while (end_date is None or period_current_date <= end_date):
63
+ for day_idx, training_day in enumerate(training_split.training_days):
64
  # Stop if we've reached the end date
65
+ if end_date and period_current_date > end_date:
66
  break
67
 
68
  scheduled_day = ScheduledTrainingDay(
69
+ date=period_current_date,
70
  training_day=training_day,
71
+ split_name=training_split.name,
72
  week_number=week_number,
73
  day_in_week=day_idx + 1
74
  )
75
  schedule.append(scheduled_day)
76
+ period_current_date += timedelta(days=1)
77
 
78
  # Increment week number after completing a full cycle
79
  week_number += 1
80
 
81
+ # For now, break after one cycle of the split to move to next period
82
+ # TODO: Add logic to determine when to move to next period based on duration
83
+ break
 
84
 
85
+ # Update current_date for the next period
86
+ current_date = period_current_date
87
 
88
  return schedule
89
 
 
123
  day_str = scheduled_day.date.strftime("%a, %b %d")
124
  intensity_str = f" ({scheduled_day.training_day.intensity.value})" if scheduled_day.training_day.intensity else ""
125
 
126
+ # Check if this is a rest day (no exercises or rest intensity)
127
+ is_rest_day = (not scheduled_day.training_day.exercises or
128
+ len(scheduled_day.training_day.exercises) == 0 or
129
+ (scheduled_day.training_day.intensity and scheduled_day.training_day.intensity.value == "rest"))
130
+
131
+ if is_rest_day:
132
  summary_lines.append(f"β€’ {day_str}: {scheduled_day.training_day.name}")
133
  else:
134
  exercise_count = len(scheduled_day.training_day.exercises) if scheduled_day.training_day.exercises else 0
 
221
  function=create_fitness_plan,
222
  prompt_instructions="""
223
  When the user requests a fitness plan, use the create_fitness_plan tool with a fully completed FitnessPlan object.
224
+
225
+ Fitness Plans are made up of Training Plans and Meal Plans that have a start and end date.
226
+ If no start date is specified, assume the plan starts today.
227
+ If no end date is given by the user assume they want a plan to be in better shape in 3 months.
228
+
229
  The FitnessPlan object must include:
230
  - name: A descriptive name for the fitness plan
231
+ - goal: The primary fitness goal (should be specific and measurable)
232
+ - description: Comprehensive overview of the plan and expected outcomes
233
+ - training_plan: A TrainingPlan object with periodized phases that includes:
234
+ * name: Name that reflects the plan's focus and target event
235
+ * description: Comprehensive overview including training philosophy, periodization strategy, target audience, expected timeline, and how the phases progress toward the goal
236
+ * training_periods: Ordered list of TrainingPeriod objects representing different phases:
237
+ - Each period should have a name (e.g., 'Base Phase', 'Advanced Phase', 'Preparation Phase')
238
+ - Include start_date for each period
239
+ - Include intensity level (rest, light, moderate, heavy, max_effort)
240
+ - Each period contains a training_split with training_days that include exercises
241
+ - Follow periodization principles: typically base building β†’ peak training β†’ recovery for optimum performance at events
242
+ - Each phase should build on the previous with appropriate progression
243
+ - meal_plan: Detailed nutrition guidance including meal suggestions, macronutrient targets, and eating schedule. Should be practical and specific to the fitness goals.
244
  - start_date: When the plan should begin (defaults to today)
245
+ - target_date: The target completion date or milestone date for the fitness plan
246
 
247
+ Training Structure:
248
+ - TrainingPeriod contains a TrainingSplit
249
+ - TrainingSplit contains multiple TrainingDay objects
250
+ - TrainingDay contains multiple Exercise objects
251
+ - Each Exercise should have name, description, and appropriate fields (sets, reps, duration, distance, intensity)
252
+ - Include rest days in the training split as needed
253
+
254
+ Create periodized plans that progress intelligently toward the goal:
255
+ - For events (hikes, competitions): Use base building β†’ strength β†’ peak β†’ taper progression
256
+ - For aesthetic goals (summer body): Use muscle building β†’ strength β†’ cutting/definition phases
257
+ - For general fitness: Use base building β†’ strength β†’ maintenance cycles
258
+
259
+ This tool automatically builds a date-based schedule from the periodized phases and saves the plan.
260
 
261
  Do not read the plan back to the user in the conversation. The user can already see it in the UI component.
262
 
shared/src/fitness_core/services/formatters.py CHANGED
@@ -24,7 +24,16 @@ class ResponseFormatter:
24
  Formatted markdown string
25
  """
26
  try:
27
- if not (hasattr(plan_obj, 'name') and
 
 
 
 
 
 
 
 
 
28
  hasattr(plan_obj, 'training_plan') and
29
  hasattr(plan_obj, 'meal_plan')):
30
  # Try to parse as string if it's not a proper object
@@ -42,6 +51,7 @@ class ResponseFormatter:
42
  else:
43
  return f"**Fitness Plan**\n\n{str(plan_obj)}"
44
 
 
45
  if style == "minimal":
46
  return f"""**{plan_obj.name}**
47
 
@@ -82,6 +92,403 @@ class ResponseFormatter:
82
  logger.error(f"Error formatting fitness plan: {str(e)}")
83
  return f"**Fitness Plan**\n\nI created a fitness plan for you, but encountered an error while formatting it. Here's the raw content:\n\n{str(plan_obj)}"
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  @staticmethod
86
  def parse_fitness_plan_from_string(plan_str: str) -> str:
87
  """
 
24
  Formatted markdown string
25
  """
26
  try:
27
+ # First, try to handle structured FitnessPlan objects
28
+ if hasattr(plan_obj, 'name') and hasattr(plan_obj, 'training_plan'):
29
+ return ResponseFormatter._format_structured_fitness_plan(plan_obj, style)
30
+
31
+ # Handle string representation of structured objects
32
+ elif isinstance(plan_obj, str) and 'training_plan_splits=' in plan_obj:
33
+ return ResponseFormatter._format_structured_plan_string(plan_obj, style)
34
+
35
+ # Legacy handling for older format objects
36
+ elif not (hasattr(plan_obj, 'name') and
37
  hasattr(plan_obj, 'training_plan') and
38
  hasattr(plan_obj, 'meal_plan')):
39
  # Try to parse as string if it's not a proper object
 
51
  else:
52
  return f"**Fitness Plan**\n\n{str(plan_obj)}"
53
 
54
+ # Legacy formatting for basic objects
55
  if style == "minimal":
56
  return f"""**{plan_obj.name}**
57
 
 
92
  logger.error(f"Error formatting fitness plan: {str(e)}")
93
  return f"**Fitness Plan**\n\nI created a fitness plan for you, but encountered an error while formatting it. Here's the raw content:\n\n{str(plan_obj)}"
94
 
95
+ @staticmethod
96
+ def _format_structured_fitness_plan(plan_obj: Any, style: str = "default") -> str:
97
+ """Format a structured FitnessPlan object with proper training plan formatting."""
98
+ try:
99
+ # Extract basic plan info
100
+ plan_name = getattr(plan_obj, 'name', 'Fitness Plan')
101
+ plan_goal = getattr(plan_obj, 'goal', '')
102
+ plan_description = getattr(plan_obj, 'description', '')
103
+ meal_plan = getattr(plan_obj, 'meal_plan', '')
104
+
105
+ # Format header
106
+ formatted = f"# πŸ‹οΈ {plan_name}\n\n"
107
+
108
+ if plan_goal:
109
+ formatted += f"**🎯 Goal:** {plan_goal}\n\n"
110
+
111
+ if plan_description:
112
+ formatted += f"**πŸ“‹ Overview:** {plan_description}\n\n"
113
+
114
+ # Format training plan
115
+ training_plan = getattr(plan_obj, 'training_plan', None)
116
+ if training_plan:
117
+ formatted += "## πŸ’ͺ Training Plan\n\n"
118
+ formatted += ResponseFormatter._format_training_plan_object(training_plan)
119
+
120
+ # Format meal plan
121
+ if meal_plan:
122
+ formatted += "\n## πŸ₯— Meal Plan\n\n"
123
+ formatted += f"{meal_plan}\n\n"
124
+
125
+ # Add footer based on style
126
+ if style in ["detailed", "default"]:
127
+ formatted += "## πŸ“Š Additional Information\n\n"
128
+ formatted += "- Plan created with AI assistance\n"
129
+ formatted += "- Customize as needed for your preferences\n"
130
+ formatted += "- Consult healthcare providers for medical advice\n\n"
131
+ formatted += "---\n"
132
+ formatted += "*Your personalized fitness plan is ready! Feel free to ask any questions about the plan or request modifications.*"
133
+
134
+ return formatted
135
+
136
+ except Exception as e:
137
+ logger.error(f"Error formatting structured fitness plan: {str(e)}")
138
+ return f"**Error Formatting Plan**\n\n{str(e)}"
139
+
140
+ @staticmethod
141
+ def _format_training_plan_object(training_plan: Any) -> str:
142
+ """Format a TrainingPlan object with proper structure."""
143
+ try:
144
+ formatted = ""
145
+
146
+ # Get training plan details
147
+ plan_name = getattr(training_plan, 'name', 'Training Plan')
148
+ plan_description = getattr(training_plan, 'description', '')
149
+
150
+ formatted += f"**{plan_name}**\n\n"
151
+ if plan_description:
152
+ formatted += f"{plan_description}\n\n"
153
+
154
+ # Format training splits
155
+ training_splits = getattr(training_plan, 'training_plan_splits', [])
156
+ for split in training_splits:
157
+ formatted += ResponseFormatter._format_training_split_object(split)
158
+
159
+ return formatted
160
+
161
+ except Exception as e:
162
+ logger.error(f"Error formatting training plan object: {str(e)}")
163
+ return f"Error formatting training plan: {str(e)}"
164
+
165
+ @staticmethod
166
+ def _format_training_split_object(split: Any) -> str:
167
+ """Format a TrainingPlanSplit object."""
168
+ try:
169
+ formatted = ""
170
+
171
+ # Get split details
172
+ split_name = getattr(split, 'name', 'Training Split')
173
+ split_description = getattr(split, 'description', '')
174
+
175
+ formatted += f"### πŸ“… {split_name}\n\n"
176
+ if split_description:
177
+ formatted += f"{split_description}\n\n"
178
+
179
+ # Format training days
180
+ training_days = getattr(split, 'training_days', [])
181
+ for day in training_days:
182
+ formatted += ResponseFormatter._format_training_day_object(day)
183
+ formatted += "\n"
184
+
185
+ return formatted
186
+
187
+ except Exception as e:
188
+ logger.error(f"Error formatting training split object: {str(e)}")
189
+ return f"Error formatting training split: {str(e)}"
190
+
191
+ @staticmethod
192
+ def _format_training_day_object(day: Any) -> str:
193
+ """Format a TrainingDay object."""
194
+ try:
195
+ formatted = ""
196
+
197
+ # Get day details
198
+ day_name = getattr(day, 'name', 'Training Day')
199
+ day_description = getattr(day, 'description', '')
200
+ day_order = getattr(day, 'order_number', '')
201
+ is_rest_day = getattr(day, 'rest_day', False)
202
+ day_intensity = getattr(day, 'intensity', None)
203
+
204
+ # Format day header
205
+ day_emoji = "😴" if is_rest_day else "πŸ’ͺ"
206
+ formatted += f"#### {day_emoji} Day {day_order}: {day_name}\n\n"
207
+
208
+ if day_description:
209
+ formatted += f"*{day_description}*\n\n"
210
+
211
+ if day_intensity and not is_rest_day:
212
+ intensity_emojis = {
213
+ 'light': '🟒', 'LIGHT': '🟒',
214
+ 'moderate': '🟑', 'MODERATE': '🟑',
215
+ 'heavy': 'πŸ”΄', 'HEAVY': 'πŸ”΄',
216
+ 'max_effort': 'πŸ”₯', 'MAX_EFFORT': 'πŸ”₯'
217
+ }
218
+ # Handle both enum objects and string representations
219
+ intensity_str = str(day_intensity).replace('IntensityLevel.', '').replace('<', '').replace('>', '').split(':')[0]
220
+ intensity_emoji = intensity_emojis.get(intensity_str, 'βšͺ')
221
+ formatted += f"**Intensity:** {intensity_emoji} {intensity_str.title()}\n\n"
222
+
223
+ # Format exercises
224
+ if not is_rest_day:
225
+ exercises = getattr(day, 'exercises', [])
226
+ if exercises:
227
+ formatted += "**Exercises:**\n\n"
228
+ for i, exercise in enumerate(exercises, 1):
229
+ formatted += ResponseFormatter._format_exercise_object(exercise, i)
230
+ formatted += "\n"
231
+ else:
232
+ formatted += "**Rest Day** - Focus on recovery, light stretching, or gentle activities.\n\n"
233
+
234
+ return formatted
235
+
236
+ except Exception as e:
237
+ logger.error(f"Error formatting training day object: {str(e)}")
238
+ return f"Error formatting training day: {str(e)}"
239
+
240
+ @staticmethod
241
+ def _format_exercise_object(exercise: Any, number: int) -> str:
242
+ """Format an Exercise object."""
243
+ try:
244
+ # Get exercise details
245
+ name = getattr(exercise, 'name', 'Exercise')
246
+ description = getattr(exercise, 'description', '')
247
+ sets = getattr(exercise, 'sets', None)
248
+ reps = getattr(exercise, 'reps', None)
249
+ duration = getattr(exercise, 'duration', None)
250
+ intensity = getattr(exercise, 'intensity', None)
251
+
252
+ formatted = f"{number}. **{name}**\n"
253
+
254
+ # Add sets/reps/duration info
255
+ workout_details = []
256
+ if sets:
257
+ workout_details.append(f"{sets} sets")
258
+ if reps:
259
+ workout_details.append(f"{reps} reps")
260
+ if duration:
261
+ workout_details.append(f"{duration}s")
262
+
263
+ if workout_details:
264
+ formatted += f" *{' Γ— '.join(workout_details)}*"
265
+
266
+ if intensity:
267
+ intensity_emojis = {
268
+ 'light': '🟒', 'LIGHT': '🟒',
269
+ 'moderate': '🟑', 'MODERATE': '🟑',
270
+ 'heavy': 'πŸ”΄', 'HEAVY': 'πŸ”΄',
271
+ 'max_effort': 'πŸ”₯', 'MAX_EFFORT': 'πŸ”₯'
272
+ }
273
+ # Handle both enum objects and string representations
274
+ intensity_str = str(intensity).replace('IntensityLevel.', '').replace('<', '').replace('>', '').split(':')[0]
275
+ intensity_emoji = intensity_emojis.get(intensity_str, 'βšͺ')
276
+ formatted += f" - {intensity_emoji} {intensity_str.title()}"
277
+
278
+ formatted += "\n"
279
+
280
+ if description:
281
+ formatted += f" *{description}*\n"
282
+
283
+ return formatted
284
+
285
+ except Exception as e:
286
+ logger.error(f"Error formatting exercise object: {str(e)}")
287
+ return f"Error formatting exercise: {str(e)}"
288
+
289
+ @staticmethod
290
+ def _format_structured_plan_string(plan_str: str, style: str = "default") -> str:
291
+ """Format a string representation of a structured fitness plan."""
292
+ try:
293
+ import re
294
+
295
+ # Extract plan name
296
+ name_match = re.search(r"πŸ‹οΈ\s*([^\n]*?)Training Plan", plan_str)
297
+ if not name_match:
298
+ name_match = re.search(r"name='([^']*)'", plan_str)
299
+ plan_name = name_match.group(1).strip() if name_match else "Fitness Plan"
300
+
301
+ # Extract meal plan section
302
+ meal_match = re.search(r"πŸ₯— Meal Plan\n(.*?)(?=πŸ“Š|$)", plan_str, re.DOTALL)
303
+ meal_plan = meal_match.group(1).strip() if meal_match else ""
304
+
305
+ # Extract training plan section with structured data
306
+ formatted = f"# πŸ‹οΈ {plan_name}\n\n"
307
+
308
+ # Parse and format the structured training data
309
+ training_match = re.search(r"training_plan_splits=\[(.*?)\]\]", plan_str, re.DOTALL)
310
+ if training_match:
311
+ formatted += "## πŸ’ͺ Training Plan\n\n"
312
+ formatted += ResponseFormatter._parse_and_format_training_data(training_match.group(1))
313
+
314
+ # Add meal plan
315
+ if meal_plan:
316
+ formatted += "\n## πŸ₯— Meal Plan\n\n"
317
+ formatted += f"{meal_plan}\n\n"
318
+
319
+ # Add footer based on style
320
+ if style in ["detailed", "default"]:
321
+ formatted += "## πŸ“Š Additional Information\n\n"
322
+ formatted += "- Plan created with AI assistance\n"
323
+ formatted += "- Customize as needed for your preferences\n"
324
+ formatted += "- Consult healthcare providers for medical advice\n\n"
325
+ formatted += "---\n"
326
+ formatted += "*Your personalized fitness plan is ready! Feel free to ask any questions about the plan or request modifications.*"
327
+
328
+ return formatted
329
+
330
+ except Exception as e:
331
+ logger.error(f"Error parsing structured plan string: {str(e)}")
332
+ return f"**Error Parsing Structured Plan**\n\n{str(e)}\n\nRaw content:\n{plan_str}"
333
+
334
+ @staticmethod
335
+ def _parse_and_format_training_data(training_data: str) -> str:
336
+ """Parse and format the training data from string representation."""
337
+ try:
338
+ import re
339
+
340
+ formatted = ""
341
+
342
+ # Extract split information
343
+ split_name_match = re.search(r"name='([^']*)'", training_data)
344
+ split_name = split_name_match.group(1) if split_name_match else "Weekly Split"
345
+
346
+ split_desc_match = re.search(r"description='([^']*)'", training_data)
347
+ split_desc = split_desc_match.group(1) if split_desc_match else ""
348
+
349
+ formatted += f"**{split_name}**\n\n"
350
+ if split_desc:
351
+ formatted += f"{split_desc}\n\n"
352
+
353
+ # Extract training days
354
+ days_pattern = r"TrainingDay\((.*?)\)(?=, TrainingDay\(|$)"
355
+ days = re.findall(days_pattern, training_data, re.DOTALL)
356
+
357
+ for day_data in days:
358
+ formatted += ResponseFormatter._parse_and_format_day_data(day_data)
359
+ formatted += "\n"
360
+
361
+ return formatted
362
+
363
+ except Exception as e:
364
+ logger.error(f"Error parsing training data: {str(e)}")
365
+ return f"Error parsing training data: {str(e)}"
366
+
367
+ @staticmethod
368
+ def _parse_and_format_day_data(day_data: str) -> str:
369
+ """Parse and format a single training day from string representation."""
370
+ try:
371
+ import re
372
+
373
+ # Extract day details
374
+ name_match = re.search(r"name='([^']*)'", day_data)
375
+ day_name = name_match.group(1) if name_match else "Training Day"
376
+
377
+ order_match = re.search(r"order_number=(\d+)", day_data)
378
+ day_order = order_match.group(1) if order_match else "1"
379
+
380
+ desc_match = re.search(r"description='([^']*)'", day_data)
381
+ day_description = desc_match.group(1) if desc_match else ""
382
+
383
+ rest_match = re.search(r"rest_day=(\w+)", day_data)
384
+ is_rest_day = rest_match and rest_match.group(1) == "True"
385
+
386
+ intensity_match = re.search(r"intensity=<IntensityLevel\.(\w+):", day_data)
387
+ day_intensity = intensity_match.group(1) if intensity_match else None
388
+
389
+ # Format day header
390
+ day_emoji = "😴" if is_rest_day else "πŸ’ͺ"
391
+ formatted = f"#### {day_emoji} Day {day_order}: {day_name}\n\n"
392
+
393
+ if day_description:
394
+ formatted += f"*{day_description}*\n\n"
395
+
396
+ if day_intensity and not is_rest_day:
397
+ intensity_emojis = {
398
+ 'LIGHT': '🟒',
399
+ 'MODERATE': '🟑',
400
+ 'HEAVY': 'πŸ”΄',
401
+ 'MAX_EFFORT': 'πŸ”₯'
402
+ }
403
+ intensity_emoji = intensity_emojis.get(day_intensity, 'βšͺ')
404
+ formatted += f"**Intensity:** {intensity_emoji} {day_intensity.title()}\n\n"
405
+
406
+ # Parse exercises if not rest day
407
+ if not is_rest_day and 'exercises=[' in day_data:
408
+ exercises_match = re.search(r"exercises=\[(.*?)\]", day_data, re.DOTALL)
409
+ if exercises_match:
410
+ exercises_data = exercises_match.group(1)
411
+ formatted += "**Exercises:**\n\n"
412
+ formatted += ResponseFormatter._parse_and_format_exercises(exercises_data)
413
+ elif is_rest_day:
414
+ formatted += "**Rest Day** - Focus on recovery, light stretching, or gentle activities.\n\n"
415
+
416
+ return formatted
417
+
418
+ except Exception as e:
419
+ logger.error(f"Error parsing day data: {str(e)}")
420
+ return f"Error parsing day data: {str(e)}"
421
+
422
+ @staticmethod
423
+ def _parse_and_format_exercises(exercises_data: str) -> str:
424
+ """Parse and format exercises from string representation."""
425
+ try:
426
+ import re
427
+
428
+ formatted = ""
429
+
430
+ # Extract individual exercises
431
+ exercise_pattern = r"Exercise\((.*?)\)(?=, Exercise\(|$)"
432
+ exercises = re.findall(exercise_pattern, exercises_data, re.DOTALL)
433
+
434
+ for i, exercise_data in enumerate(exercises, 1):
435
+ # Extract exercise details
436
+ name_match = re.search(r"name='([^']*)'", exercise_data)
437
+ name = name_match.group(1) if name_match else "Exercise"
438
+
439
+ desc_match = re.search(r"description='([^']*)'", exercise_data)
440
+ description = desc_match.group(1) if desc_match else ""
441
+
442
+ sets_match = re.search(r"sets=(\d+)", exercise_data)
443
+ sets = sets_match.group(1) if sets_match else None
444
+
445
+ reps_match = re.search(r"reps=(\d+)", exercise_data)
446
+ reps = reps_match.group(1) if reps_match else None
447
+
448
+ duration_match = re.search(r"duration=(\d+)", exercise_data)
449
+ duration = duration_match.group(1) if duration_match else None
450
+
451
+ intensity_match = re.search(r"intensity=<IntensityLevel\.(\w+):", exercise_data)
452
+ intensity = intensity_match.group(1) if intensity_match else None
453
+
454
+ # Format exercise
455
+ formatted += f"{i}. **{name}**\n"
456
+
457
+ # Add workout details
458
+ workout_details = []
459
+ if sets:
460
+ workout_details.append(f"{sets} sets")
461
+ if reps:
462
+ workout_details.append(f"{reps} reps")
463
+ if duration:
464
+ workout_details.append(f"{duration}s")
465
+
466
+ if workout_details:
467
+ formatted += f" *{' Γ— '.join(workout_details)}*"
468
+
469
+ if intensity:
470
+ intensity_emojis = {
471
+ 'LIGHT': '🟒',
472
+ 'MODERATE': '🟑',
473
+ 'HEAVY': 'πŸ”΄',
474
+ 'MAX_EFFORT': 'πŸ”₯'
475
+ }
476
+ intensity_emoji = intensity_emojis.get(intensity, 'βšͺ')
477
+ formatted += f" - {intensity_emoji} {intensity.title()}"
478
+
479
+ formatted += "\n"
480
+
481
+ if description:
482
+ formatted += f" *{description}*\n"
483
+
484
+ formatted += "\n"
485
+
486
+ return formatted
487
+
488
+ except Exception as e:
489
+ logger.error(f"Error parsing exercises: {str(e)}")
490
+ return f"Error parsing exercises: {str(e)}"
491
+
492
  @staticmethod
493
  def parse_fitness_plan_from_string(plan_str: str) -> str:
494
  """
shared/src/fitness_core/utils/config.py CHANGED
@@ -18,7 +18,7 @@ class Config:
18
  DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
19
 
20
  # AI Model configuration
21
- DEFAULT_MODEL: str = os.getenv("AI_MODEL", os.getenv("OPENAI_MODEL", "llama-3.3-70b-versatile"))
22
  ANTHROPIC_API_KEY: Optional[str] = os.getenv("ANTHROPIC_API_KEY")
23
  OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")
24
 
 
18
  DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
19
 
20
  # AI Model configuration
21
+ DEFAULT_MODEL: str = os.getenv("AI_MODEL", os.getenv("OPENAI_MODEL", "gpt-4o"))
22
  ANTHROPIC_API_KEY: Optional[str] = os.getenv("ANTHROPIC_API_KEY")
23
  OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")
24
 
test_formatting.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for structured fitness plan formatting
4
+ """
5
+
6
+ # Sample structured output from your example
7
+ sample_output = """πŸ‹οΈ Intermediate Muscle Hypertrophy Accelerator
8
+ πŸ’ͺ Training Plan
9
+ name='Muscle Hypertrophy Split' description='A 5-day split targeting different muscle groups with high volume and strategic intensity progression to maximize muscle growth.' training_plan_splits=[TrainingPlanSplit(name='Muscle Building Weekly Split', order=1, description='Advanced split focusing on targeted muscle group development with progressive overload techniques', start_date=datetime.date(2024, 2, 15), training_days=[TrainingDay(name='Chest and Triceps Day', order_number=1, description='High-volume chest and triceps workout with compound and isolation movements', exercises=[Exercise(name='Barbell Bench Press', description='Perform with a controlled tempo, focusing on chest muscle engagement', duration=None, sets=4, reps=8, intensity=<IntensityLevel.HEAVY: 'heavy'>), Exercise(name='Incline Dumbbell Press', description='Focus on full range of motion and mind-muscle connection', duration=None, sets=3, reps=10, intensity=<IntensityLevel.MODERATE: 'moderate'>), Exercise(name='Tricep Pushdowns', description='Keep elbows close to body, use a controlled movement', duration=None, sets=3, reps=12, intensity=<IntensityLevel.MODERATE: 'moderate'>)], intensity=<IntensityLevel.HEAVY: 'heavy'>, rest_day=False), TrainingDay(name='Back and Biceps Day', order_number=2, description='Comprehensive back and biceps muscle development workout', exercises=[Exercise(name='Deadlifts', description='Maintain proper form, engage core and back muscles', duration=None, sets=4, reps=6, intensity=<IntensityLevel.HEAVY: 'heavy'>), Exercise(name='Pull-ups', description='Use full range of motion, can use assisted pull-up machine if needed', duration=None, sets=3, reps=8, intensity=<IntensityLevel.MODERATE: 'moderate'>), Exercise(name='Barbell Curls', description='Controlled movement, focus on bicep contraction', duration=None, sets=3, reps=10, intensity=<IntensityLevel.MODERATE: 'moderate'>)], intensity=<IntensityLevel.HEAVY: 'heavy'>, rest_day=False), TrainingDay(name='Leg Day', order_number=3, description='Intense lower body workout targeting quads, hamstrings, and glutes', exercises=[Exercise(name='Barbell Squats', description='Maintain proper depth and form, engage core', duration=None, sets=4, reps=8, intensity=<IntensityLevel.HEAVY: 'heavy'>), Exercise(name='Romanian Deadlifts', description='Focus on hamstring stretch and contraction', duration=None, sets=3, reps=10, intensity=<IntensityLevel.MODERATE: 'moderate'>), Exercise(name='Leg Press', description='Full range of motion, controlled tempo', duration=None, sets=3, reps=12, intensity=<IntensityLevel.MODERATE: 'moderate'>)], intensity=<IntensityLevel.HEAVY: 'heavy'>, rest_day=False), TrainingDay(name='Shoulder Day', order_number=4, description='Comprehensive shoulder muscle development with varied movements', exercises=[Exercise(name='Overhead Military Press', description='Strict form, engage core for stability', duration=None, sets=4, reps=8, intensity=<IntensityLevel.HEAVY: 'heavy'>), Exercise(name='Lateral Raises', description='Controlled movement, avoid swinging', duration=None, sets=3, reps=12, intensity=<IntensityLevel.MODERATE: 'moderate'>), Exercise(name='Face Pulls', description='Focus on rear deltoid engagement', duration=None, sets=3, reps=15, intensity=<IntensityLevel.LIGHT: 'light'>)], intensity=<IntensityLevel.HEAVY: 'heavy'>, rest_day=False), TrainingDay(name='Arms and Core Day', order_number=5, description='Targeted arm muscle development with core strengthening', exercises=[Exercise(name='Close Grip Bench Press', description='Emphasize tricep engagement', duration=None, sets=3, reps=10, intensity=<IntensityLevel.MODERATE: 'moderate'>), Exercise(name='Hammer Curls', description='Controlled bicep curl variation', duration=None, sets=3, reps=12, intensity=<IntensityLevel.MODERATE: 'moderate'>), Exercise(name='Planks', description='Maintain proper form, engage entire core', duration=60, sets=3, reps=None, intensity=<IntensityLevel.MODERATE: 'moderate'>)], intensity=<IntensityLevel.MODERATE: 'moderate'>, rest_day=False), TrainingDay(name='Rest and Recovery', order_number=6, description='Complete rest day for muscle recovery and growth', exercises=None, intensity=None, rest_day=True), TrainingDay(name='Rest and Recovery', order_number=7, description='Complete rest day for muscle recovery and growth', exercises=None, intensity=None, rest_day=True)])]
10
+
11
+ πŸ₯— Meal Plan
12
+ High-protein diet focusing on lean proteins, complex carbohydrates, and healthy fats. Aim for 1.6-2.2g of protein per kg of body weight. Consume 300-500 calories above maintenance level. Meal examples: Chicken breast with brown rice and vegetables, salmon with sweet potato, protein smoothies, egg white omelets with whole grain toast. Supplement with whey protein and creatine monohydrate.
13
+
14
+ πŸ“Š Additional Information
15
+ Plan created with AI assistance
16
+ Customize as needed for your preferences
17
+ Consult healthcare providers for medical advice
18
+ Your personalized fitness plan is ready! Feel free to ask any questions about the plan or request modifications."""
19
+
20
+ # Import the formatter
21
+ import sys
22
+ import os
23
+ sys.path.append(os.path.join(os.path.dirname(__file__), 'shared', 'src'))
24
+
25
+ from fitness_core.services.formatters import ResponseFormatter
26
+
27
+ # Test the formatter
28
+ print("=== Testing Structured Fitness Plan Formatting ===\n")
29
+
30
+ try:
31
+ formatted_result = ResponseFormatter.format_fitness_plan(sample_output, style="detailed")
32
+ print("FORMATTED OUTPUT:")
33
+ print(formatted_result)
34
+ except Exception as e:
35
+ print(f"Error: {e}")
36
+ print(f"Raw input: {sample_output[:200]}...")