Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| from mcp_server import handle_tool_call | |
| import re | |
| import json | |
| # --- Load Shelf Life Data --- | |
| with open("spoilage_data.json", "r") as f: | |
| shelf_life_data = json.load(f) | |
| # --- Backend Logic Wrapping --- | |
| def run_all_tools(text): | |
| parsed = handle_tool_call("parse_ingredients", {"text": text}) | |
| spoilage = handle_tool_call("predict_spoilage", {"items": parsed}) | |
| carbon = handle_tool_call("estimate_carbon", {"items": parsed}) | |
| items_with_risk = [ | |
| {**item, "risk": spoilage[i]} for i, item in enumerate(parsed) | |
| ] | |
| meal_plan = handle_tool_call( | |
| "generate_meal_plan", | |
| {"items": items_with_risk, "spoilage": spoilage, "carbon": carbon, "parsed": parsed} | |
| ) | |
| print(meal_plan) | |
| return parsed, spoilage, carbon, meal_plan | |
| # --- Output Formatters --- | |
| def format_items_table(meal_plan_json): | |
| if isinstance(meal_plan_json, str): | |
| try: | |
| meal_plan_json = json.loads(meal_plan_json) | |
| except json.JSONDecodeError: | |
| pass | |
| ingredient_data = [] | |
| if isinstance(meal_plan_json, list): | |
| for block in meal_plan_json: | |
| if isinstance(block, dict): | |
| for val in block.values(): | |
| if isinstance(val, str): | |
| match = re.search(r"```json\n(.*?)```", val, re.DOTALL) | |
| if not match: | |
| match = re.search(r"```(.*?)```", val, re.DOTALL) | |
| if match: | |
| try: | |
| extracted_json = json.loads(match.group(1)) | |
| if isinstance(extracted_json, dict): | |
| ingredient_data = extracted_json.get("ingredientDetails", []) | |
| elif isinstance(extracted_json, list): | |
| ingredient_data = extracted_json | |
| break | |
| except json.JSONDecodeError: | |
| continue | |
| if not ingredient_data: | |
| return "<div style='color: gray;'>No ingredient data available in meal plan.</div>" | |
| html = """ | |
| <style> | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| margin-bottom: 30px; | |
| } | |
| th, td { | |
| border: 1px solid #ddd; | |
| padding: 10px; | |
| text-align: left; | |
| } | |
| th { | |
| background-color: #2a9d8f; | |
| color: white; | |
| } | |
| .expiry-short { color: #d62828; font-weight: bold; } | |
| .expiry-medium { color: #f77f00; font-weight: bold; } | |
| .expiry-long { color: #2a9d8f; font-weight: bold; } | |
| </style> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Ingredient</th> | |
| <th>Expiry / Shelf Life</th> | |
| <th>Spoilage Risk</th> | |
| <th>Carbon Footprint (kg COβe/kg)</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| for item in ingredient_data: | |
| name = item.get("Ingredient", "Unknown").title() | |
| expiry_display = item.get("Expiry / Shelf Life", "N/A") | |
| risk = item.get("Spoilage Risk", "Unknown").capitalize() | |
| carbon_fp_str = str(item.get("Carbon Footprint", "N/A")).replace(" kg COβe/kg", "").strip() | |
| days_match = re.search(r"(\d+)", expiry_display) | |
| if days_match: | |
| days = int(days_match.group(1)) | |
| if days <= 2: | |
| expiry_class = "expiry-short" | |
| icon = "β°" | |
| elif days <= 5: | |
| expiry_class = "expiry-medium" | |
| icon = "π" | |
| else: | |
| expiry_class = "expiry-long" | |
| icon = "β " | |
| else: | |
| expiry_class = "" | |
| icon = "β" | |
| expiry_html = f'<span class="{expiry_class}">{icon} {expiry_display}</span>' | |
| risk_color = {"High": "red", "Medium": "orange", "Low": "green"}.get(risk, "gray") | |
| try: | |
| carbon_value = float(carbon_fp_str) | |
| intensity = "π" * int(min(carbon_value / 0.5, 5)) | |
| carbon_html = f"{carbon_value:.2f} {intensity}" | |
| except ValueError: | |
| carbon_html = "π«οΈ N/A" | |
| html += f""" | |
| <tr> | |
| <td><b>{name}</b></td> | |
| <td>{expiry_html}</td> | |
| <td style="color:{risk_color}; font-weight:bold;">{risk}</td> | |
| <td>{carbon_html}</td> | |
| </tr> | |
| """ | |
| html += "</tbody></table>" | |
| return html | |
| def format_meal_output(meal_plan_json): | |
| import uuid | |
| if isinstance(meal_plan_json, str): | |
| try: | |
| meal_plan_json = json.loads(meal_plan_json) | |
| except json.JSONDecodeError: | |
| return "<div style='color: red;'>Invalid meal plan format.</div>" | |
| combined_text = "" | |
| for block in meal_plan_json: | |
| for val in block.values(): | |
| if isinstance(val, str): | |
| combined_text += "\n" + val | |
| sections = { | |
| "b": {"title": "π Carbon Footprint Overview", "content": ""}, | |
| "c": {"title": "π Suggested Meals", "content": ""}, | |
| "d": {"title": "π Alternative Ingredients", "content": ""}, | |
| "e": {"title": "π§ Reducing Waste & Emissions", "content": ""} | |
| } | |
| matches = re.findall(r"### \((b|c|d|e)\) (.+?)\n(.*?)(?=\n### \(|\Z)", combined_text, re.DOTALL) | |
| for code, heading, content in matches: | |
| if code in sections: | |
| sections[code]["content"] = content.strip() | |
| # Dark-mode accordion style | |
| style = """ | |
| <style> | |
| .accordion { | |
| background-color: #121212; | |
| border-radius: 8px; | |
| margin-bottom: 10px; | |
| border: 1px solid #333; | |
| overflow: hidden; | |
| } | |
| .accordion summary { | |
| font-weight: bold; | |
| cursor: pointer; | |
| padding: 15px; | |
| font-size: 1.1rem; | |
| background: #1e1e1e; | |
| color: #fff; | |
| } | |
| .accordion p { | |
| padding: 15px; | |
| margin: 0; | |
| background: #1a1a1a; | |
| color: #ddd; | |
| line-height: 1.6; | |
| } | |
| </style> | |
| """ | |
| html_parts = [] | |
| for sec in ["b", "c", "d", "e"]: | |
| content = sections[sec]["content"] | |
| if not content: | |
| continue | |
| content_html = content.replace("\n", "<br>") | |
| html_parts.append(f""" | |
| <details class="accordion"> | |
| <summary>{sections[sec]["title"]}</summary> | |
| <p>{content_html}</p> | |
| </details> | |
| """) | |
| return style + "\n".join(html_parts) | |
| def combined_output_func(text, api_key): | |
| status_text = "β³ Processing..." | |
| parsed = handle_tool_call("parse_ingredients", {"text": text}) | |
| spoilage = handle_tool_call("predict_spoilage", {"items": parsed}) | |
| carbon = handle_tool_call("estimate_carbon", {"items": parsed}) | |
| items_with_risk = [ | |
| {**item, "risk": spoilage[i]} for i, item in enumerate(parsed) | |
| ] | |
| meal_plan = handle_tool_call( | |
| "generate_meal_plan", | |
| { | |
| "items": items_with_risk, | |
| "spoilage": spoilage, | |
| "carbon": carbon, | |
| "parsed": parsed, | |
| "api_key": api_key | |
| } | |
| ) | |
| # Check for connection error inside 'recipes' | |
| if ( | |
| isinstance(meal_plan, list) and | |
| len(meal_plan) >= 2 and | |
| isinstance(meal_plan[1], dict) and | |
| "recipes" in meal_plan[1] and | |
| meal_plan[1]["recipes"] == ['Error generating recipes: Connection error.'] | |
| ): | |
| status_text = "β Connection error: Please input correct API key" | |
| error_html = """ | |
| <div style="background-color: #ffdddd; color: #900; padding: 16px; margin-bottom: 20px; border-left: 6px solid #f44336; border-radius: 6px;"> | |
| β οΈ <strong>Connection error:</strong> Please input a correct Nebius API key and try again. | |
| </div> | |
| """ | |
| return status_text, error_html | |
| else: | |
| status_text = "β Success! EcoChef has generated your meal plan and sustainability report." | |
| success_banner = """ | |
| <div style="background-color: #ddffdd; color: #064b1a; padding: 16px; margin-bottom: 20px; border-left: 6px solid #4CAF50; border-radius: 6px;"> | |
| π± <strong>Success!</strong> Your meal plan and carbon footprint report have been generated. | |
| </div> | |
| """ | |
| if isinstance(meal_plan, str): | |
| try: | |
| meal_plan = json.loads(meal_plan) | |
| except json.JSONDecodeError: | |
| meal_plan = [] | |
| table_html = format_items_table(meal_plan) | |
| meal_html = format_meal_output(meal_plan) | |
| full_html = f""" | |
| {success_banner} | |
| {table_html} | |
| <hr style="margin:40px 0; border-top: 2px solid #ccc;"> | |
| {meal_html} | |
| """ | |
| return status_text, full_html | |
| # Continue as usual if no error | |
| if isinstance(meal_plan, str): | |
| try: | |
| meal_plan = json.loads(meal_plan) | |
| except json.JSONDecodeError: | |
| meal_plan = [] | |
| status_text = "β Done!" | |
| table_html = format_items_table(meal_plan) | |
| meal_html = format_meal_output(meal_plan) | |
| full_html = f""" | |
| {table_html} | |
| <hr style="margin:40px 0; border-top: 2px solid #ccc;"> | |
| {meal_html} | |
| """ | |
| return status_text, full_html | |
| # --- Gradio UI --- | |
| with gr.Blocks(title="EcoChef Dashboard", theme=gr.themes.Base(primary_hue="green", secondary_hue="blue")) as demo: | |
| gr.Markdown("# π₯ **EcoChef β Eco-Friendly AI Meal Planner with Food Carbon Insights**") | |
| gr.Markdown("> _Reduce food waste. Eat smarter and Save the planet._") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| user_input = gr.Textbox( | |
| lines=4, | |
| placeholder="e.g. 2 bananas, LOBSTER, 6 eggs, rice, lentils", | |
| value="Bananas 2, Lobsters 2, rice 1kg, lentils 500gm, yogurt expiring on 12th june, pasta expiring on 20th june", | |
| label="π§ What's in your fridge or pantry?" | |
| ) | |
| gr.Markdown("_Note: **You may add expiry date of a product as: expiring on**_") | |
| nebius_key_input = gr.Textbox( | |
| placeholder="Paste your Nebius AI API key here...", | |
| label="π Enter Nebius AI API Key", | |
| type="password" | |
| ) | |
| submit_btn = gr.Button("β¨ Analyze Ingredients") | |
| status = gr.Markdown("β³", visible=False) | |
| gr.Markdown(" Carbon footprint data taken from the SU-EATABLE LIFE dataset.") | |
| gr.Markdown(" π€ Created for Gradio Agents & MCP Hackathon 2025") | |
| with gr.Column(scale=2): | |
| combined_output = gr.HTML(label="Ingredients & Meal Plan") | |
| submit_btn.click( | |
| combined_output_func, | |
| inputs=[user_input, nebius_key_input], | |
| outputs=[status, combined_output] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(mcp_server=True) | |