File size: 6,442 Bytes
d061432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os, time

def query_model(prompt, max_tokens=1500):
    """Single Groq API call."""
    GROQ_API_KEY = os.getenv("GROQ_API_KEY", "").strip()

    if not GROQ_API_KEY:
        raise ValueError(
            "GROQ_API_KEY is not set.\n\n"
            "Steps to fix:\n"
            "1. Go to https://console.groq.com β†’ sign up free\n"
            "2. Click 'API Keys' β†’ 'Create API Key'\n"
            "3. Copy the key (starts with gsk_...)\n"
            "4. In your HuggingFace Space β†’ Settings β†’ Variables and Secrets\n"
            "   β†’ Add secret: Name = GROQ_API_KEY, Value = gsk_xxxxxxxxxxxx\n"
            "5. Restart the Space"
        )

    try:
        from groq import Groq
        client = Groq(api_key=GROQ_API_KEY)

        response = client.chat.completions.create(
            model="llama-3.3-70b-versatile",   # fastest + best quality on Groq
            messages=[
                {
                    "role": "system",
                    "content": (
                        "You are a certified professional fitness trainer. "
                        "Provide detailed, structured, day-by-day workout plans exactly as instructed."
                    )
                },
                {"role": "user", "content": prompt}
            ],
            max_tokens=max_tokens,
            temperature=0.7,
        )
        return response.choices[0].message.content

    except Exception as e:
        err = str(e)

        if "401" in err or "invalid_api_key" in err.lower() or "authentication" in err.lower():
            raise ValueError(
                "❌ Groq API key is invalid or expired.\n\n"
                "Fix: Go to https://console.groq.com/keys β†’ create a new key β†’ "
                "update GROQ_API_KEY in your HuggingFace Space Secrets."
            ) from None

        if "429" in err or "rate_limit" in err.lower() or "rate limit" in err.lower():
            raise ValueError(
                "⏳ Groq rate limit hit. Wait 60 seconds and try again.\n\n"
                "Free tier: 6,000 tokens/min. Upgrade at console.groq.com for higher limits."
            ) from None

        if "503" in err or "504" in err or "timeout" in err.lower():
            raise ValueError(
                "⚑ Groq server busy. Please try again in a few seconds."
            ) from None

        raise


def query_model_chunked(name, gender, height, weight, goal, fitness_level,
                         equipment, days_per_week=5, months=1,
                         progress_callback=None):
    """
    Generate a long plan by splitting into chunks of 3 days each.
    Groq is fast enough (~2-3s per chunk) so even 30-day plans finish in ~30s.
    progress_callback(chunk_num, total_chunks, days_done, total_days) β€” optional.
    Returns: (full_plan_text, bmi, bmi_cat)
    """
    from prompt_builder import calculate_bmi, bmi_category, bmi_advice

    bmi      = calculate_bmi(weight, height)
    bmi_cat  = bmi_category(bmi)
    advice   = bmi_advice(bmi_cat)

    total_days = min(days_per_week * 4 * months, 30)
    eq_list    = ", ".join(equipment) if equipment else "Bodyweight only"

    intensity_map = {
        "Beginner":     "2–3 sets, moderate weight, 90s rest. Prioritise form.",
        "Intermediate": "3–4 sets, progressive overload, 60–75s rest.",
        "Advanced":     "4–5 sets, heavy compounds, 45–60s rest, supersets.",
    }
    intensity = intensity_map.get(fitness_level, "3 sets, 60s rest.")

    CHUNK_SIZE   = 3       # 3 days per call β€” well within Groq token limits
    chunks       = []
    start_day    = 1
    total_chunks = max(1, (total_days + CHUNK_SIZE - 1) // CHUNK_SIZE)

    while start_day <= total_days:
        end_day   = min(start_day + CHUNK_SIZE - 1, total_days)
        chunk_num = len(chunks) + 1

        if progress_callback:
            progress_callback(chunk_num, total_chunks, start_day - 1, total_days)

        context_note = ""
        if chunks:
            context_note = (
                f"(Continue from Day {start_day}. Do NOT repeat Days 1–{start_day-1}. "
                f"Vary muscle groups from previous days.)"
            )

        prompt = f"""Fitness trainer. Generate ONLY Days {start_day}-{end_day} of a {total_days}-day plan.
Client: {name}, {gender}, {height}cm/{weight}kg, BMI {bmi:.1f} ({bmi_cat}), Goal: {goal}, Level: {fitness_level}, Equipment: {eq_list}.
{context_note}

For EACH day use EXACTLY this format:
## Day N - [Muscle Group]
**Warm-Up** - Exercise: 2x10, Exercise: 2x10
**Main Workout**
- ExerciseName β€” 3x12 reps (rest 60s)
- ExerciseName β€” 3x12 reps (rest 60s)
- ExerciseName β€” 3x12 reps (rest 60s)
- ExerciseName β€” 3x12 reps (rest 60s)
- ExerciseName β€” 3x12 reps (rest 60s)
**Cool-Down** - Stretch1, Stretch2

Rules: vary muscle groups each day, {fitness_level} appropriate, use {eq_list} only, intensity: {intensity}.
{"End with 1 motivational sentence for "+name+"." if end_day == total_days else "No motivational text β€” more days follow."}
Output ONLY Days {start_day}-{end_day}. No preamble. No extra text.
"""

        # Retry up to 3 times on transient errors
        for attempt in range(3):
            try:
                chunk_text = query_model(prompt, max_tokens=1500)
                break
            except ValueError as e:
                # Rate limit β€” wait and retry
                err = str(e)
                if "rate limit" in err.lower() and attempt < 2:
                    wait = 65  # wait just over 1 minute for rate limit window to reset
                    if progress_callback:
                        progress_callback(chunk_num, total_chunks, start_day - 1, total_days,
                                          status=f"Rate limit β€” waiting {wait}s…")
                    time.sleep(wait)
                    continue
                raise
            except Exception as e:
                err = str(e)
                if attempt < 2:
                    time.sleep(5 * (attempt + 1))
                    continue
                raise

        chunks.append(chunk_text.strip())
        start_day = end_day + 1

        # Small pause between chunks to respect rate limits on free tier
        if start_day <= total_days:
            time.sleep(1)

    if progress_callback:
        progress_callback(total_chunks, total_chunks, total_days, total_days)

    full_plan = "\n\n".join(chunks)
    return full_plan, bmi, bmi_cat