293droid commited on
Commit
fd13291
·
1 Parent(s): 365c885

fix parsing of prompts

Browse files
Files changed (1) hide show
  1. app.py +129 -1
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
- parsed_recipe = parse_recipe(user_input)
 
 
 
 
 
 
 
 
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