dailymate / app.py
DailyMateWebsite's picture
Update app.py
6368332 verified
import json
import os
import traceback
import requests
from flask import Flask, jsonify, request, send_from_directory
app = Flask(__name__, static_folder="site")
HF_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions"
RESEND_API_URL = "https://api.resend.com/emails"
TEXT_MODEL = os.getenv("TEXT_MODEL", "Qwen/Qwen2.5-7B-Instruct")
VISION_MODEL = os.getenv("VISION_MODEL", "CohereLabs/aya-vision-32b:cohere")
def get_hf_token():
return (
os.getenv("HF_TOKEN", "").strip()
or os.getenv("HF_API_KEY", "").strip()
)
def get_resend_api_key():
return os.getenv("RESEND_API_KEY", "").strip()
def clean_value(value, fallback="not specified"):
if value is None:
return fallback
text = str(value).strip()
if not text or text.lower() in {"unknown", "all", "prefer not to say", "not specified"}:
return fallback
return text
def require_hf_token():
hf_token_raw = os.getenv("HF_TOKEN", "")
hf_api_key_raw = os.getenv("HF_API_KEY", "")
hf_token_exists = bool(hf_token_raw.strip())
hf_api_key_exists = bool(hf_api_key_raw.strip())
print("HF_TOKEN exists:", hf_token_exists, flush=True)
print("HF_API_KEY exists:", hf_api_key_exists, flush=True)
if not (hf_token_exists or hf_api_key_exists):
raise ValueError(
f"DEBUG_TOKEN_CHECK | HF_TOKEN_exists={hf_token_exists} | "
f"HF_API_KEY_exists={hf_api_key_exists}"
)
def hf_chat_completion(model, messages, max_tokens=700, temperature=0.3):
require_hf_token()
token = get_hf_token()
response = requests.post(
HF_ROUTER_URL,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
},
timeout=180,
)
if not response.ok:
raise ValueError(f"HF router error {response.status_code}: {response.text}")
data = response.json()
choices = data.get("choices", [])
if not choices:
return ""
message = choices[0].get("message", {})
content = message.get("content", "")
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
parts.append(item.get("text", ""))
return "\n".join(part for part in parts if part).strip()
return str(content).strip()
def build_prompt(data):
goal = clean_value(data.get("goal"), "general fitness")
level = clean_value(data.get("level"), "any level")
training_location = clean_value(data.get("training_location"), "anywhere")
workout_time = clean_value(data.get("workout_time"), "30")
age_range = clean_value(data.get("age"), "not specified")
sex = clean_value(data.get("sex"), "not specified")
height_range = clean_value(data.get("height_range"), "not specified")
weight_range = clean_value(data.get("weight_range"), "not specified")
extra_notes = clean_value(data.get("extra_notes"), "none")
estimated_bmi = data.get("estimated_bmi")
bmi_text = "not available"
if estimated_bmi not in (None, "", "unknown"):
bmi_text = str(estimated_bmi)
return f"""
You are a fitness recommendation assistant for a website called DailyMate.
Create a practical, structured, personalized 7-day fitness plan.
User profile:
- Goal: {goal}
- Experience level: {level}
- Training location: {training_location}
- Minutes available per workout: {workout_time}
- Age range: {age_range}
- Sex: {sex}
- Height range: {height_range}
- Weight range: {weight_range}
- Estimated BMI: {bmi_text}
- Additional details: {extra_notes}
Important instructions:
- Use the structured profile above in a meaningful way.
- If estimated BMI suggests the person may be overweight or underweight, adjust intensity, recovery, and nutrition tone sensibly.
- Treat BMI only as a rough support signal, not as a diagnosis.
- Respect any limitations, allergies, equipment, pain, or injuries mentioned in Additional details.
- Do not give medical advice.
- Do not diagnose.
- Do not use generic motivational filler.
- Do not add disclaimers.
- Do not say "Okay User" or similar chatbot phrases.
- Keep the plan realistic for the user's likely level and body profile.
- Make the weekly plan actually vary across the week.
- Include nutrition guidance and recovery advice, not only workouts.
Return the answer in this exact structure:
Overview:
Write 2 short sentences describing the overall direction of the program.
7-Day Plan:
Monday: ...
Tuesday: ...
Wednesday: ...
Thursday: ...
Friday: ...
Saturday: ...
Sunday: ...
Nutrition Guidance:
Write 2-4 sentences with practical nutrition guidance based on the user's goal, estimated BMI, sex, weight range, and any food-related notes.
Recovery Advice:
Write 2-4 sentences with realistic recovery advice based on the user's level, age range, and limitations.
Summary:
Write 2 short sentences summarizing the whole weekly plan and what the user should focus on.
Formatting rules:
- Plain text only
- No markdown
- No bullet symbols except the weekday labels
- No asterisks
- Keep it concise but useful
""".strip()
def build_support_prompt(message, history, context):
goal = clean_value(context.get("goal"), "general fitness")
level = clean_value(context.get("level"), "any level")
training_location = clean_value(context.get("training_location"), "anywhere")
workout_time = clean_value(context.get("workout_time"), "not specified")
age_range = clean_value(context.get("age_range"), "not specified")
sex = clean_value(context.get("sex"), "not specified")
height_range = clean_value(context.get("height_range"), "not specified")
weight_range = clean_value(context.get("weight_range"), "not specified")
extra_notes = clean_value(context.get("extra_notes"), "none")
estimated_bmi = context.get("estimated_bmi")
latest_plan = context.get("latest_plan") or {}
bmi_text = "not available"
if estimated_bmi not in (None, "", "unknown"):
bmi_text = str(estimated_bmi)
history_lines = []
for item in history[-8:]:
role = str(item.get("role", "")).strip().lower()
content = str(item.get("content", "")).strip()
if role in {"user", "assistant"} and content:
history_lines.append(f"{role.capitalize()}: {content}")
history_block = "\n".join(history_lines) if history_lines else "No previous chat history."
latest_plan_text = "No generated plan available yet."
if latest_plan:
latest_plan_text = f"""
Goal label: {latest_plan.get('goalLabel', 'General fitness')}
Overview: {latest_plan.get('overview', '')}
7-Day Plan: {' | '.join(latest_plan.get('weekly', []))}
Nutrition Guidance: {latest_plan.get('nutrition', '')}
Recovery Advice: {latest_plan.get('recovery', '')}
Summary: {latest_plan.get('summary', '')}
""".strip()
return f"""
You are DailyMate Support, a focused assistant for a fitness planning website.
Your job:
- Answer only DailyMate-related support questions and simple fitness-plan guidance.
- Help with using the app, understanding generated plans, rest days, workout swaps, time limits, training location changes, and beginner-friendly clarifications.
- Use the user's current planner context when useful.
- If the user asks for something unrelated to DailyMate, briefly say that you only handle DailyMate support and fitness-plan questions.
- Do not give medical advice.
- Do not diagnose.
- Keep answers practical, clear, and short.
- Usually answer in 2 to 5 sentences.
- If a plan has not been generated yet, say so when relevant.
- If the question is about replacing an exercise, suggest a reasonable equivalent based on home/gym context.
- If asked whether food or a habit is “good or bad,” give a balanced practical answer instead of moralizing.
Current planner context:
- Goal: {goal}
- Experience level: {level}
- Training location: {training_location}
- Workout time: {workout_time}
- Age range: {age_range}
- Sex: {sex}
- Height range: {height_range}
- Weight range: {weight_range}
- Estimated BMI: {bmi_text}
- Additional details: {extra_notes}
Latest generated plan:
{latest_plan_text}
Recent chat history:
{history_block}
Current user question:
{message}
Answer as DailyMate Support:
""".strip()
def build_meal_analysis_prompt():
return """
You are analyzing a food photo for a fitness website called DailyMate.
This is a STRICT visual recognition task.
Rules:
- Only describe food that is actually visible in the image.
- Never invent ingredients, side dishes, sauces, or extra foods that cannot be clearly seen.
- If the image shows only one obvious food, return only one item.
- If the image shows multiple clearly visible foods, split them into separate items.
- If you are unsure what a food is, use a cautious label like "unknown pastry", "unknown fruit", or "unknown food item".
- Do not assume this is a balanced meal.
- Do not add foods from memory or from typical meal patterns.
- Focus on what is visibly present, not what might normally come with it.
- Estimate calories conservatively and approximately.
- Base calorie estimates on visible portion size only.
- If the image is unclear, return fewer items rather than guessing extra items.
Healthiness labeling rules:
- "generally healthy" = mostly whole/minimally processed food, reasonable portion, not obviously heavy in sugar/frying/cream
- "moderately healthy" = mixed or neutral, okay in moderation, somewhat calorie-dense or processed
- "occasional / heavier choice" = very sugary, fried, pastry-like, creamy, fast-food-like, or clearly calorie-dense
Return only valid JSON in exactly this structure:
{
"visible_food_summary": "short literal description of what is visibly in the image",
"items": [
{
"name": "food item name",
"estimated_calories": 0,
"note": "short portion-based reason"
}
],
"total_estimated_calories": 0,
"healthiness_label": "generally healthy",
"summary": "Two short sentences."
}
Important output rules:
- The visible_food_summary must literally describe what is seen.
- The items list must match the visible_food_summary.
- If the image appears to show bananas, do not output chicken or vegetables.
- If the image appears to show donuts, do not output chicken or vegetables.
- If the image appears to show pasta only, do not invent steak or salad.
- If the image appears to show a burger and fries, do not output donut or pastry.
- Do not include markdown.
- Do not include any text before or after the JSON.
""".strip()
def try_parse_meal_json(text):
raw = str(text or "").strip()
if not raw:
return None
if raw.startswith("```"):
raw = raw.strip("`")
raw = raw.replace("json", "", 1).strip()
try:
return json.loads(raw)
except json.JSONDecodeError:
pass
start = raw.find("{")
end = raw.rfind("}")
if start != -1 and end != -1 and end > start:
try:
return json.loads(raw[start:end + 1])
except json.JSONDecodeError:
return None
return None
def normalize_meal_result(parsed):
if not isinstance(parsed, dict):
return {
"visible_food_summary": "Could not clearly identify the visible food.",
"items": [],
"total_estimated_calories": 0,
"healthiness_label": "moderately healthy",
"summary": "The meal could not be estimated clearly from the image, so treat this as a rough visual guess."
}
items = parsed.get("items", [])
if not isinstance(items, list):
items = []
normalized_items = []
for item in items[:8]:
if not isinstance(item, dict):
continue
name = str(item.get("name", "")).strip() or "Food item"
note = str(item.get("note", "")).strip()
estimated_calories = item.get("estimated_calories", 0)
try:
estimated_calories = int(round(float(estimated_calories)))
except (TypeError, ValueError):
estimated_calories = 0
normalized_items.append({
"name": name,
"estimated_calories": max(0, estimated_calories),
"note": note
})
total_estimated_calories = parsed.get("total_estimated_calories", 0)
try:
total_estimated_calories = int(round(float(total_estimated_calories)))
except (TypeError, ValueError):
total_estimated_calories = sum(item["estimated_calories"] for item in normalized_items)
healthiness_label = str(parsed.get("healthiness_label", "moderately healthy")).strip().lower()
if healthiness_label not in {
"generally healthy",
"moderately healthy",
"occasional / heavier choice"
}:
healthiness_label = "moderately healthy"
summary = str(parsed.get("summary", "")).strip()
if not summary:
summary = "This is a rough meal estimate based on visible ingredients. Hidden oils, sauces, and true portion size can change the actual calories."
visible_food_summary = str(parsed.get("visible_food_summary", "")).strip()
if not visible_food_summary:
visible_food_summary = "Visible food items were estimated from the image."
return {
"visible_food_summary": visible_food_summary,
"items": normalized_items,
"total_estimated_calories": max(0, total_estimated_calories),
"healthiness_label": healthiness_label,
"summary": summary
}
def validate_email_payload(data):
email = str(data.get("email", "")).strip()
plan_text = str(data.get("plan_text", "")).strip()
goal = str(data.get("goal", "Fitness")).strip()
name = str(data.get("name", "")).strip()
weekly_ideas_opt_in = bool(data.get("weekly_ideas_opt_in", False))
if not email:
return False, "Email address is required.", None
if "@" not in email or "." not in email:
return False, "Please provide a valid email address.", None
if not plan_text:
return False, "No plan content was provided for email sending.", None
return True, "", {
"email": email,
"plan_text": plan_text,
"goal": goal or "Fitness",
"name": name,
"weekly_ideas_opt_in": weekly_ideas_opt_in
}
def send_plan_email(recipient_email, recipient_name, goal, plan_text, weekly_ideas_opt_in=False):
resend_api_key = get_resend_api_key()
if not resend_api_key:
raise ValueError("RESEND_API_KEY is missing. Add it as a Secret in your Hugging Face Space settings.")
subject_goal = goal if goal else "Fitness"
greeting_name = recipient_name if recipient_name else "there"
marketing_note_text = ""
marketing_note_html = ""
if weekly_ideas_opt_in:
marketing_note_text = (
"\nYou also asked to receive weekly fitness ideas and updates. "
"That preference was recorded on the request.\n"
)
marketing_note_html = (
"<p>You also asked to receive weekly fitness ideas and updates. "
"That preference was recorded on the request.</p>"
)
text_body = f"""Hi {greeting_name},
Here is your DailyMate personalized fitness plan.
{plan_text}
{marketing_note_text}
Stay consistent,
DailyMate
"""
html_body = f"""
<div style="font-family: Arial, Helvetica, sans-serif; line-height: 1.6; color: #111;">
<h2>Your DailyMate plan — {subject_goal}</h2>
<p>Hi {greeting_name},</p>
<p>Here is your DailyMate personalized fitness plan.</p>
<pre style="white-space: pre-wrap; font-family: Arial, Helvetica, sans-serif; background: #f6f6f6; padding: 16px; border-radius: 8px;">{plan_text}</pre>
{marketing_note_html}
<p>Stay consistent,<br>DailyMate</p>
</div>
"""
payload = {
"from": "DailyMate <onboarding@resend.dev>",
"to": [recipient_email],
"subject": f"Your DailyMate plan — {subject_goal}",
"text": text_body,
"html": html_body,
}
response = requests.post(
RESEND_API_URL,
headers={
"Authorization": f"Bearer {resend_api_key}",
"Content-Type": "application/json",
},
json=payload,
timeout=30,
)
if not response.ok:
raise ValueError(f"Resend API error {response.status_code}: {response.text}")
@app.route("/")
def home():
return send_from_directory("site", "index.html")
@app.route("/recommend", methods=["POST"])
def recommend():
try:
data = request.get_json(force=True)
prompt = build_prompt(data)
recommendation = hf_chat_completion(
model=TEXT_MODEL,
messages=[
{"role": "system", "content": "You are DailyMate's fitness planning assistant."},
{"role": "user", "content": prompt},
],
max_tokens=900,
temperature=0.5,
)
return jsonify({
"recommendation": recommendation or "No response from model."
})
except requests.exceptions.Timeout:
return jsonify({
"error": "AI took too long to respond. Please try again."
}), 500
except requests.exceptions.HTTPError as exc:
traceback.print_exc()
print("RECOMMEND HTTP ERROR:", repr(exc), flush=True)
return jsonify({
"error": f"Model API returned an HTTP error: {exc}"
}), 500
except Exception as exc:
traceback.print_exc()
print("RECOMMEND ERROR:", repr(exc), flush=True)
return jsonify({
"error": str(exc)
}), 500
@app.route("/support-chat", methods=["POST"])
def support_chat():
try:
data = request.get_json(force=True)
message = str(data.get("message", "")).strip()
history = data.get("history", [])
context = data.get("context", {})
if not message:
return jsonify({"error": "Message is required."}), 400
if not isinstance(history, list):
history = []
if not isinstance(context, dict):
context = {}
prompt = build_support_prompt(message, history, context)
reply = hf_chat_completion(
model=TEXT_MODEL,
messages=[
{"role": "system", "content": "You are DailyMate Support."},
{"role": "user", "content": prompt},
],
max_tokens=500,
temperature=0.3,
)
if not reply:
reply = "Sorry, I could not generate a support answer right now."
return jsonify({
"reply": reply
})
except requests.exceptions.Timeout:
return jsonify({
"error": "DailyMate support took too long to respond. Please try again."
}), 500
except requests.exceptions.HTTPError as exc:
traceback.print_exc()
print("SUPPORT HTTP ERROR:", repr(exc), flush=True)
return jsonify({
"error": f"Model API returned an HTTP error: {exc}"
}), 500
except Exception as exc:
traceback.print_exc()
print("SUPPORT ERROR:", repr(exc), flush=True)
return jsonify({
"error": str(exc)
}), 500
@app.route("/analyze-meal", methods=["POST"])
def analyze_meal():
try:
data = request.get_json(force=True)
image_base64 = str(data.get("image_base64", "")).strip()
if not image_base64:
return jsonify({"error": "Meal image is required."}), 400
prompt = build_meal_analysis_prompt()
image_data_url = f"data:image/jpeg;base64,{image_base64}"
raw_response = hf_chat_completion(
model=VISION_MODEL,
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": image_data_url}},
],
}
],
max_tokens=700,
temperature=0.1,
)
parsed = try_parse_meal_json(raw_response)
normalized = normalize_meal_result(parsed)
return jsonify(normalized)
except requests.exceptions.Timeout:
return jsonify({
"error": "Meal analysis took too long to respond. Try a smaller image or try again."
}), 500
except requests.exceptions.HTTPError as exc:
traceback.print_exc()
print("MEAL HTTP ERROR:", repr(exc), flush=True)
return jsonify({
"error": f"Model API returned an HTTP error: {exc}"
}), 500
except Exception as exc:
traceback.print_exc()
print("MEAL ERROR:", repr(exc), flush=True)
return jsonify({
"error": str(exc)
}), 500
@app.route("/send-plan", methods=["POST"])
def send_plan():
try:
data = request.get_json(force=True)
is_valid, error_message, cleaned = validate_email_payload(data)
if not is_valid:
return jsonify({"error": error_message}), 400
send_plan_email(
recipient_email=cleaned["email"],
recipient_name=cleaned["name"],
goal=cleaned["goal"],
plan_text=cleaned["plan_text"],
weekly_ideas_opt_in=cleaned["weekly_ideas_opt_in"]
)
message = f"Your plan was sent to {cleaned['email']}."
if cleaned["weekly_ideas_opt_in"]:
message += " Weekly fitness ideas preference was included."
return jsonify({
"message": message
})
except ValueError as exc:
return jsonify({
"error": str(exc)
}), 500
except Exception as exc:
return jsonify({
"error": f"Unexpected error while sending email: {exc}"
}), 500
@app.route("/debug-env")
def debug_env():
return jsonify({
"HF_TOKEN_exists": bool(os.getenv("HF_TOKEN")),
"HF_API_KEY_exists": bool(os.getenv("HF_API_KEY")),
"TEXT_MODEL": os.getenv("TEXT_MODEL", "Qwen/Qwen2.5-7B-Instruct"),
"VISION_MODEL": os.getenv("VISION_MODEL", "CohereLabs/aya-vision-32b:cohere"),
"HF_TOKEN_length": len(os.getenv("HF_TOKEN", "").strip()),
"HF_API_KEY_length": len(os.getenv("HF_API_KEY", "").strip()),
"RESEND_API_KEY_exists": bool(os.getenv("RESEND_API_KEY")),
"RESEND_API_KEY_length": len(os.getenv("RESEND_API_KEY", "").strip())
})
if __name__ == "__main__":
port = int(os.getenv("PORT", "7860"))
app.run(host="0.0.0.0", port=port)