Spaces:
Sleeping
Sleeping
293droid commited on
Commit ·
fd13291
1
Parent(s): 365c885
fix parsing of prompts
Browse files
app.py
CHANGED
|
@@ -6,6 +6,7 @@ Tiny Titan eligible (0.5B parameters).
|
|
| 6 |
|
| 7 |
import os
|
| 8 |
import re
|
|
|
|
| 9 |
import gradio as gr
|
| 10 |
from huggingface_hub import InferenceClient
|
| 11 |
from chemistry import (
|
|
@@ -37,6 +38,40 @@ and suggest one simple fix.
|
|
| 37 |
End your response after giving your advice. Do not ask follow-up questions. Do not say 'How do you want to proceed' or similar."""
|
| 38 |
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
# AI call
|
| 42 |
# ---------------------------------------------------------------------------
|
|
@@ -124,6 +159,91 @@ def detect_mode(text: str) -> str:
|
|
| 124 |
return "recipe"
|
| 125 |
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
# ---------------------------------------------------------------------------
|
| 128 |
# Mode handlers
|
| 129 |
# ---------------------------------------------------------------------------
|
|
@@ -602,7 +722,15 @@ Your water should not meaningfully interfere with lather or leave extra mineral
|
|
| 602 |
|
| 603 |
|
| 604 |
def run_soap_lab(user_input: str, vegan_only: bool, superfat: float = 5.0, water_hardness: str = "Soft") -> tuple[str, str, str, str]:
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
total_oil_weight = sum(parsed_recipe.values()) if parsed_recipe else None
|
| 607 |
water_panel = water_hardness_recommendation(water_hardness, total_oil_weight)
|
| 608 |
|
|
|
|
| 6 |
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
+
import json
|
| 10 |
import gradio as gr
|
| 11 |
from huggingface_hub import InferenceClient
|
| 12 |
from chemistry import (
|
|
|
|
| 38 |
End your response after giving your advice. Do not ask follow-up questions. Do not say 'How do you want to proceed' or similar."""
|
| 39 |
|
| 40 |
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Extraction prompt (LLM -> structured JSON)
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
EXTRACTION_SYSTEM_PROMPT = """You are an extraction assistant. Your job is to extract ingredients and weights
|
| 45 |
+
from a user's free-form text and return a single JSON object only (no explanation).
|
| 46 |
+
|
| 47 |
+
Requirements:
|
| 48 |
+
- If the user provided a recipe or ingredient list, return exactly:
|
| 49 |
+
{"type": "recipe", "ingredients": [{"name": "<ingredient name>", "weight_g": <number>}, ...]}
|
| 50 |
+
- If the user did NOT provide ingredients (they asked a question or for advice), return exactly:
|
| 51 |
+
{"type": "advice"}
|
| 52 |
+
- Normalise ALL weight formats to grams (g). Accept formats like: "300g", "300 grams",
|
| 53 |
+
"0.3kg", "10oz", "1 lb", "1lb", "ounces", "kgs", and convert them to grams.
|
| 54 |
+
Use 1 oz = 28.3495 g, 1 lb = 453.59237 g, 1 kg = 1000 g.
|
| 55 |
+
- Use numeric grams (floats allowed). Round to one decimal place only when necessary.
|
| 56 |
+
- Return only JSON; do not include any trailing text or markdown.
|
| 57 |
+
- Use deterministic output: set sampling temperature to 0.0 (this will be enforced by the caller).
|
| 58 |
+
|
| 59 |
+
Few-shot examples (input -> JSON):
|
| 60 |
+
|
| 61 |
+
Input: "300g coconut oil, 200g olive oil, 100g shea butter"
|
| 62 |
+
Output: {"type": "recipe", "ingredients": [{"name": "coconut oil", "weight_g": 300}, {"name": "olive oil", "weight_g": 200}, {"name": "shea butter", "weight_g": 100}]}
|
| 63 |
+
|
| 64 |
+
Input: "0.3kg olive oil and 10oz coconut oil"
|
| 65 |
+
Output: {"type": "recipe", "ingredients": [{"name": "olive oil", "weight_g": 300}, {"name": "coconut oil", "weight_g": 283.5}]}
|
| 66 |
+
|
| 67 |
+
Input: "I have 1 lb coconut oil and 250 g olive oil"
|
| 68 |
+
Output: {"type": "recipe", "ingredients": [{"name": "coconut oil", "weight_g": 453.6}, {"name": "olive oil", "weight_g": 250}]}
|
| 69 |
+
|
| 70 |
+
Input: "How can I make a gentle soap for dry sensitive skin?"
|
| 71 |
+
Output: {"type": "advice"}
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
|
| 75 |
# ---------------------------------------------------------------------------
|
| 76 |
# AI call
|
| 77 |
# ---------------------------------------------------------------------------
|
|
|
|
| 159 |
return "recipe"
|
| 160 |
|
| 161 |
|
| 162 |
+
# ---------------------------------------------------------------------------
|
| 163 |
+
# LLM extraction and JSON validation
|
| 164 |
+
# ---------------------------------------------------------------------------
|
| 165 |
+
|
| 166 |
+
def _find_json_in_text(text: str) -> str | None:
|
| 167 |
+
m = re.search(r"\{.*\}", text, re.DOTALL)
|
| 168 |
+
return m.group(0) if m else None
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _map_ingredients_to_recipe(ingredients: list[dict]) -> dict[str, float] | None:
|
| 172 |
+
recipe = {}
|
| 173 |
+
for item in ingredients:
|
| 174 |
+
name = (item.get("name") or "").lower()
|
| 175 |
+
weight = item.get("weight_g")
|
| 176 |
+
try:
|
| 177 |
+
weight = float(weight)
|
| 178 |
+
except Exception:
|
| 179 |
+
return None
|
| 180 |
+
if weight <= 0:
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
matched_key = None
|
| 184 |
+
for alias, key in OIL_ALIASES.items():
|
| 185 |
+
if alias in name:
|
| 186 |
+
matched_key = key
|
| 187 |
+
break
|
| 188 |
+
if not matched_key:
|
| 189 |
+
for key in OILS:
|
| 190 |
+
if key in name:
|
| 191 |
+
matched_key = key
|
| 192 |
+
break
|
| 193 |
+
|
| 194 |
+
if not matched_key:
|
| 195 |
+
# Could not map ingredient to a known oil
|
| 196 |
+
return None
|
| 197 |
+
|
| 198 |
+
recipe[matched_key] = recipe.get(matched_key, 0.0) + weight
|
| 199 |
+
|
| 200 |
+
return recipe
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def extract_with_llm(user_input: str) -> tuple[str, dict | None]:
|
| 204 |
+
"""Call the LLM to extract ingredients as JSON. Returns (type, recipe_or_none).
|
| 205 |
+
type is 'recipe' or 'advice'. If extraction fails or JSON is invalid, returns ('advice', None).
|
| 206 |
+
"""
|
| 207 |
+
try:
|
| 208 |
+
response = client.chat_completion(
|
| 209 |
+
model=MODEL,
|
| 210 |
+
messages=[
|
| 211 |
+
{"role": "system", "content": EXTRACTION_SYSTEM_PROMPT},
|
| 212 |
+
{"role": "user", "content": user_input},
|
| 213 |
+
],
|
| 214 |
+
max_tokens=400,
|
| 215 |
+
temperature=0.0,
|
| 216 |
+
)
|
| 217 |
+
content = response.choices[0].message.content
|
| 218 |
+
except Exception:
|
| 219 |
+
return ("advice", None)
|
| 220 |
+
|
| 221 |
+
# Try to extract JSON from the model output
|
| 222 |
+
json_text = None
|
| 223 |
+
try:
|
| 224 |
+
json_text = _find_json_in_text(content) or content
|
| 225 |
+
parsed = json.loads(json_text)
|
| 226 |
+
except Exception:
|
| 227 |
+
return ("advice", None)
|
| 228 |
+
|
| 229 |
+
if not isinstance(parsed, dict) or "type" not in parsed:
|
| 230 |
+
return ("advice", None)
|
| 231 |
+
|
| 232 |
+
if parsed["type"] == "advice":
|
| 233 |
+
return ("advice", None)
|
| 234 |
+
|
| 235 |
+
if parsed["type"] == "recipe":
|
| 236 |
+
ingredients = parsed.get("ingredients")
|
| 237 |
+
if not isinstance(ingredients, list):
|
| 238 |
+
return ("advice", None)
|
| 239 |
+
mapped = _map_ingredients_to_recipe(ingredients)
|
| 240 |
+
if not mapped:
|
| 241 |
+
return ("advice", None)
|
| 242 |
+
return ("recipe", mapped)
|
| 243 |
+
|
| 244 |
+
return ("advice", None)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
# ---------------------------------------------------------------------------
|
| 248 |
# Mode handlers
|
| 249 |
# ---------------------------------------------------------------------------
|
|
|
|
| 722 |
|
| 723 |
|
| 724 |
def run_soap_lab(user_input: str, vegan_only: bool, superfat: float = 5.0, water_hardness: str = "Soft") -> tuple[str, str, str, str]:
|
| 725 |
+
# First, ask the LLM to extract a structured recipe JSON (temperature=0.0)
|
| 726 |
+
extraction_type, extracted = extract_with_llm(user_input)
|
| 727 |
+
if extraction_type == "advice":
|
| 728 |
+
# Extraction determined this is an advice request or extraction failed.
|
| 729 |
+
advice_context = "The user did not provide a parseable recipe. Provide concise, practical soap-making advice based on their question."
|
| 730 |
+
advice = ask_model(user_input, advice_context)
|
| 731 |
+
return "", "", advice, water_hardness_recommendation(water_hardness, None)
|
| 732 |
+
|
| 733 |
+
parsed_recipe = extracted or {}
|
| 734 |
total_oil_weight = sum(parsed_recipe.values()) if parsed_recipe else None
|
| 735 |
water_panel = water_hardness_recommendation(water_hardness, total_oil_weight)
|
| 736 |
|