import os import requests import time import logging import json import re from typing import Union, Optional, Dict, Any, List class InferenceModel: def __init__(self): self.hf_url = "https://Nishath2025-qwen-finetuned.hf.space/generate" self.logger = logging.getLogger(__name__) self.timeout = 120 # 2 minutes timeout self.max_retries = 2 def _extract_user_allergies(self, user_profile: str) -> List[str]: """Extract allergens from user profile text""" if not user_profile: return [] print(f"šŸ” Extracting allergies from profile: '{user_profile}'") # Common allergen keywords to look for in user profile allergen_keywords = [ 'peanut', 'peanuts', 'tree nut', 'tree nuts', 'nuts', 'milk', 'dairy', 'lactose', 'casein', 'whey', 'egg', 'eggs', 'albumin', 'soy', 'soya', 'soybean', 'wheat', 'gluten', 'barley', 'rye', 'oats', 'fish', 'salmon', 'tuna', 'cod', 'shellfish', 'shrimp', 'crab', 'lobster', 'oyster', 'sesame', 'mustard', 'celery', 'lupin', 'walnut', 'almond', 'cashew', 'pistachio', 'pecan', 'hazelnut' ] profile_lower = user_profile.lower() found_allergens = [] for allergen in allergen_keywords: # Look for patterns like "allergic to X", "X allergy", "avoid X", etc. patterns = [ rf'\b{allergen}\b', rf'allergic to {allergen}', rf'{allergen} allergy', rf'avoid {allergen}', rf'intolerant to {allergen}', rf'{allergen} intolerance' ] for pattern in patterns: if re.search(pattern, profile_lower): found_allergens.append(allergen) print(f"āœ… Found allergen: {allergen}") break print(f"šŸ“‹ Total allergens found: {found_allergens}") return list(set(found_allergens)) # Remove duplicates def _check_allergen_match(self, ingredients: str, user_allergies: List[str]) -> tuple: """ Check if any user allergies are present in ingredients. Returns (has_match, matched_allergens) """ if not user_allergies or not ingredients: return False, [] print(f"šŸ” Checking ingredients '{ingredients}' against allergies {user_allergies}") ingredients_lower = ingredients.lower() matched_allergens = [] for allergen in user_allergies: # Check if allergen appears in ingredients allergen_patterns = [ rf'\b{allergen}\b', rf'{allergen}s\b', # plural form rf'\b{allergen[:-1]}s\b' if allergen.endswith('s') else rf'\b{allergen}s\b' # handle plural ] for pattern in allergen_patterns: if re.search(pattern, ingredients_lower): matched_allergens.append(allergen) print(f"🚨 ALLERGEN MATCH FOUND: {allergen}") break has_match = len(matched_allergens) > 0 print(f"šŸ›”ļø Allergen check result: has_match={has_match}, matched={matched_allergens}") return has_match, matched_allergens def _hf_generate(self, prompt: str) -> Optional[str]: """Low-level call to the HF Space /generate endpoint. Returns text or None.""" try: resp = requests.post(self.hf_url, json={"prompt": prompt}, timeout=self.timeout) if resp.status_code != 200: self.logger.warning(f"HF returned {resp.status_code}: {resp.text[:200]}") return None data = resp.json() return data.get("response") or data.get("analysis") or None except Exception as e: self.logger.warning(f"HF call failed: {e}") return None def predict_huggingface(self, prompt, max_new_tokens=300): """ Generate prediction using Hugging Face Qwen model and return raw output (no post-processing). """ start_time = time.time() try: raw_response = self._hf_generate(prompt) processing_time = time.time() - start_time # Fallback if no response if not raw_response or raw_response.strip() in {"No response generated", ""}: return { "response": """SAFETY STATUS: SAFE SAFETY ASSESSMENT: • Unable to analyze ingredients due to model response issue • These appear to be common food ingredients • No immediate safety concerns identified RECOMMENDATIONS: • Check ingredient labels for personal allergens • Consult healthcare provider for specific dietary concerns • Monitor for any adverse reactions""", "status": "success", "model_used": "fallback-safe", "prompt": prompt, "processing_time": processing_time } # Clean up the response - remove unwanted instructional text cleaned_response = raw_response # Remove the specific problematic text and similar variations unwanted_texts = [ "If there is no need for recommendations, don't provide them.", "If there is no need for recommendations, don't provide them", "If there is no need for recommendations, do not provide them.", "If there is no need for recommendations, do not provide them", "If no recommendations are needed, don't provide them.", "If no recommendations are needed, don't provide them", "Don't provide recommendations if not needed.", "Don't provide recommendations if not needed" ] for unwanted_text in unwanted_texts: cleaned_response = cleaned_response.replace(unwanted_text, "").strip() # Remove any text that appears before the actual analysis # Look for patterns like "OVERVIEW", "ANALYSIS", etc. followed by the unwanted text import re # Remove any instructional text that might appear at the beginning cleaned_response = re.sub(r'^.*?(?=SAFETY\s*STATUS\s*:|SAFETY\s*ASSESSMENT\s*:)', '', cleaned_response, flags=re.IGNORECASE | re.DOTALL).strip() # Try to find SAFETY STATUS first match = re.search(r"(?i)safety\s*status\s*:", cleaned_response) if match: cleaned_response = cleaned_response[match.start():].strip() else: # If no SAFETY STATUS, try to find SAFETY ASSESSMENT match = re.search(r"(?i)safety\s*assessment\s*:", cleaned_response) if match: cleaned_response = "SAFETY STATUS: SAFE\n\n" + cleaned_response[match.start():].strip() # Verify the output has the required structure if not re.search(r"(?i)safety\s*assessment\s*:", cleaned_response): # Missing required sections, return default message return { "response": "Model failed to generate proper analysis. Please try again.", "status": "success", "model_used": "huggingface-qwen-fallback", "prompt": prompt, "processing_time": processing_time } return { "response": cleaned_response, "status": "success", "model_used": "huggingface-qwen", "prompt": prompt, "processing_time": processing_time } except Exception as e: processing_time = time.time() - start_time return { "response": None, "status": "error", "error": str(e), "prompt": prompt, "processing_time": processing_time } def analyze_ingredients_personalized(self, ingredients, user_profile=None): """ Analyze ingredients with personalized recommendations using intelligent allergen checking. """ print(f"\n🧬 PERSONALIZED ANALYSIS STARTING") print(f"šŸ½ļø Ingredients to analyze: {ingredients}") print(f"šŸ‘¤ User profile: {user_profile}") if user_profile: # Extract user allergies from profile user_allergies = self._extract_user_allergies(user_profile) # Check if any user allergies match the ingredients has_match, matched_allergens = self._check_allergen_match(ingredients, user_allergies) if has_match: # User has allergies that match ingredients - FORCE UNSAFE allergen_list = ", ".join(matched_allergens) print(f"🚨 ALLERGEN CONFLICT DETECTED! Forcing UNSAFE status") print(f"āš ļø Matched allergens: {allergen_list}") return { "response": f"""SAFETY STATUS: UNSAFE SAFETY ASSESSMENT: • This product contains {allergen_list}, which you are allergic to • Consuming this product poses a serious risk of allergic reaction • The allergen {allergen_list} is clearly present in the ingredients • This product is not safe for you to consume RECOMMENDATIONS: • DO NOT consume this product due to allergen presence • Check all food labels carefully for {allergen_list} before purchasing • Seek allergen-free alternatives immediately • Consult healthcare provider if accidental exposure occurs""", "status": "success", "model_used": "allergen-safety-override", "prompt": f"Allergen conflict detected: {allergen_list} in {ingredients}", "processing_time": 0.1 } else: # No allergen match - proceed with normal personalized analysis print("āœ… No allergen conflicts detected - proceeding with AI analysis") prompt = f"""You are a nutrition and food safety assistant. The user has the following health profile: {user_profile} Note: User's known allergens do not appear to be present in these ingredients. So, Output generally not with respect to that user allergies. Ingredients: {ingredients} Output strictly in this format (exactly once, no extra text): SAFETY STATUS: SAFE or UNSAFE SAFETY ASSESSMENT: • [2–4 concise bullets about the ingredients and user's health profile] RECOMMENDATIONS: • [2–4 actionable, ingredient-specific steps tailored to the user's health profile] ADDITIVES AND PRESERVATIVES: • [additives and preservatives in the product if present] Rules: - Start with 'SAFETY STATUS:' on the first line - Mark UNSAFE only if there are significant safety concerns beyond the user's known allergens - Use only the three headings above, exactly once, in this order - Do not include any separators like '---', '===', or banners - Every list item must start with '• '""" else: # No user profile - use general analysis print("šŸ‘¤ No user profile provided - using general analysis") prompt = f"""You are a nutrition and food safety assistant. Ingredients: {ingredients} Output strictly in this format (exactly once, no extra text): SAFETY STATUS: SAFE or UNSAFE SAFETY ASSESSMENT: • [2–4 concise bullets] RECOMMENDATIONS: • [2–4 actionable, ingredient-specific steps tied to the assessment] ADDITIVES AND PRESERVATIVES: • [additives and preservatives in the product if present] Rules: - Start with 'SAFETY STATUS:' on the first line. - Use only the three headings above, exactly once, in this order. - Do not include any separators like '---', '===', or banners. - Do not include any notes, disclaimers, links, or contact information. - Every list item must start with '• '. """ print(f"šŸ¤– Sending to AI model...") return self.predict_huggingface(prompt, max_new_tokens=400) def analyze_ingredients(self, ingredients): """ Analyze ingredients for general safety using only Hugging Face """ print(f"\n🧪 GENERAL ANALYSIS STARTING") print(f"šŸ½ļø Ingredients to analyze: {ingredients}") prompt = f"""You are a nutrition and food safety assistant. Ingredients: {ingredients} Output strictly in this format (exactly once, no extra text): SAFETY STATUS: SAFE or UNSAFE SAFETY ASSESSMENT: • [2–4 concise bullets] RECOMMENDATIONS: • [2–4 actionable, ingredient-specific steps tied to the assessment] ADDITIVES AND PRESERVATIVES: • [additives and preservatives in the product if present] Rules: - Start with 'SAFETY STATUS:' on the first line. - Use only the three headings above, exactly once, in this order. - Do not include any separators like '---', '===', or banners. - Do not include any notes, disclaimers, links, or contact information. - Every list item must start with '• '. """ print(f"šŸ¤– Sending to AI model...") return self.predict_huggingface(prompt, max_new_tokens=400) inference_model = InferenceModel()