1
0

app.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import streamlit as st
  2. import pymysql
  3. import myloginpath
  4. import ollama
  5. import bcrypt
  6. import requests
  7. import string
  8. import random
  9. import smtplib
  10. from email.message import EmailMessage
  11. import pandas as pd
  12. def local_web_search(query: str) -> str:
  13. try:
  14. req = requests.get(f'http://127.0.0.1:8080/search', params={'q': query, 'format': 'json'})
  15. if req.status_code == 200:
  16. data = req.json()
  17. results = data.get('results', [])
  18. if not results: return f"No results found on the web for '{query}'."
  19. snippets = [f"Source: {r.get('url')}\nContent: {r.get('content')}" for r in results[:3]]
  20. return "\n\n".join(snippets)
  21. return "Search engine returned an error."
  22. except Exception as e: return f"Local search engine unreachable: {e}"
  23. search_tool_schema = {
  24. 'type': 'function',
  25. 'function': {
  26. 'name': 'local_web_search',
  27. 'description': 'Search the internet for info not in DB.',
  28. 'parameters': {'type': 'object', 'properties': {'query': {'type': 'string'}}, 'required': ['query']},
  29. },
  30. }
  31. def get_db_connection(login_path):
  32. try:
  33. conf = myloginpath.parse(login_path)
  34. return pymysql.connect(
  35. host=conf.get('host', '127.0.0.1'),
  36. user=conf.get('user'),
  37. password=conf.get('password'),
  38. database='food_db',
  39. cursorclass=pymysql.cursors.DictCursor
  40. )
  41. except Exception as e:
  42. st.error(f"Connection Failed: {e}")
  43. return None
  44. def verify_login(username, password):
  45. conn = get_db_connection('app_auth')
  46. if not conn: return False
  47. with conn.cursor() as cursor:
  48. cursor.execute("SELECT password_hash FROM users WHERE username = %s LIMIT 1", (username,))
  49. result = cursor.fetchone()
  50. conn.close()
  51. if result: return bcrypt.checkpw(password.encode('utf-8'), result['password_hash'].encode('utf-8'))
  52. return False
  53. def get_user_id(username):
  54. conn = get_db_connection('app_auth')
  55. if not conn: return None
  56. with conn.cursor() as cursor:
  57. cursor.execute("SELECT id FROM users WHERE username = %s LIMIT 1", (username,))
  58. result = cursor.fetchone()
  59. conn.close()
  60. return result['id'] if result else None
  61. def get_eav_profile(username):
  62. uid = get_user_id(username)
  63. if not uid: return []
  64. conn = get_db_connection('app_auth')
  65. with conn.cursor() as cursor:
  66. cursor.execute("SELECT id, illness_health_condition_diet_dislikes_name as name, illness_health_condition_diet_dislikes_value as value FROM user_health_profiles WHERE user_id = %s", (uid,))
  67. res = cursor.fetchall()
  68. conn.close()
  69. return res
  70. def get_user_limit(username):
  71. conn = get_db_connection('app_auth')
  72. if not conn: return "50"
  73. with conn.cursor() as cursor:
  74. cursor.execute("SELECT search_limit FROM users WHERE username = %s LIMIT 1", (username,))
  75. result = cursor.fetchone()
  76. conn.close()
  77. return result['search_limit'] if (result and result['search_limit']) else "50"
  78. def register_user(username, password, email):
  79. conn = get_db_connection('app_auth')
  80. if not conn: return False
  81. hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
  82. try:
  83. with conn.cursor() as cursor:
  84. cursor.execute("INSERT INTO users (username, password_hash, email) VALUES (%s, %s, %s)", (username, hashed, email))
  85. conn.commit()
  86. conn.close()
  87. send_email(email, "Welcome to Local Food AI", f"Hello {username}, your account was securely created!")
  88. return True
  89. except pymysql.err.IntegrityError:
  90. return False
  91. def send_email(to_email, subject, body):
  92. try:
  93. msg = EmailMessage()
  94. msg.set_content(body)
  95. msg['Subject'] = subject
  96. msg['From'] = "security@localfoodai.com"
  97. msg['To'] = to_email
  98. s = smtplib.SMTP('localhost', 25)
  99. s.send_message(msg)
  100. s.quit()
  101. except Exception:
  102. print(f"Mock SMTP -> Sent to {to_email} | Subject: {subject}")
  103. def reset_password(username, email):
  104. conn = get_db_connection('app_auth')
  105. if not conn: return False
  106. with conn.cursor() as cursor:
  107. cursor.execute("SELECT id, email FROM users WHERE username = %s", (username,))
  108. user = cursor.fetchone()
  109. if user and user['email'] == email:
  110. new_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
  111. hashed = bcrypt.hashpw(new_pass.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
  112. cursor.execute("UPDATE users SET password_hash = %s WHERE id = %s", (hashed, user['id']))
  113. conn.commit()
  114. conn.close()
  115. send_email(email, "Password Reset", f"Your new temporary password is: {new_pass}")
  116. return True
  117. return False
  118. # UI Theming
  119. st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
  120. st.markdown("""
  121. <style>
  122. @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap');
  123. html, body, [class*="css"] { font-family: 'Inter', sans-serif; background-color: #0b192c; color: #e2e8f0; }
  124. h1, h2, h3 { color: #38bdf8 !important; font-weight: 600; letter-spacing: 0.5px; }
  125. div[data-testid="stSidebar"] { background: rgba(11, 25, 44, 0.95) !important; backdrop-filter: blur(10px); border-right: 1px solid #1e293b; }
  126. .stButton>button { background: linear-gradient(135deg, #0ea5e9, #0284c7); color: white; border: none; border-radius: 6px; }
  127. .stButton>button:hover { transform: scale(1.02); }
  128. .stTextInput>div>div>input, .stNumberInput>div>div>input, .stSelectbox>div>div>div { background-color: #0f172a; color: #f8fafc; border: 1px solid #38bdf8; }
  129. </style>
  130. """, unsafe_allow_html=True)
  131. if "authenticated_user" not in st.session_state:
  132. st.session_state["authenticated_user"] = None
  133. with st.sidebar:
  134. st.title("User Portal 🔐")
  135. if st.session_state["authenticated_user"]:
  136. st.success(f"Logged in as: {st.session_state['authenticated_user']}")
  137. if st.button("Logout"):
  138. st.session_state["authenticated_user"] = None
  139. st.rerun()
  140. st.markdown("---")
  141. st.subheader("🏥 Dynamic Health Profile")
  142. eav_data = get_eav_profile(st.session_state["authenticated_user"])
  143. uid = get_user_id(st.session_state["authenticated_user"])
  144. user_lim = get_user_limit(st.session_state["authenticated_user"])
  145. with st.expander("⚙️ Account Preferences"):
  146. opts = ["10", "20", "50", "100", "All"]
  147. idx = opts.index(user_lim) if user_lim in opts else 2
  148. new_lim = st.selectbox("Default Search Limit", opts, index=idx)
  149. if new_lim != user_lim:
  150. conn = get_db_connection('app_auth')
  151. with conn.cursor() as c:
  152. c.execute("UPDATE users SET search_limit = %s WHERE id = %s", (new_lim, uid))
  153. conn.commit()
  154. st.rerun()
  155. with st.expander("➕ Add Condition / Diet"):
  156. new_cat = st.selectbox("Category", ["Condition", "Illness", "Diet", "Dislike", "Allergy"])
  157. new_val = st.text_input("Value (e.g. 'vegan', 'diabetes', 'broccoli')").strip().lower()
  158. if st.button("Add to Profile") and new_val and uid:
  159. conn = get_db_connection('app_auth')
  160. with conn.cursor() as c:
  161. c.execute("INSERT INTO user_health_profiles (user_id, illness_health_condition_diet_dislikes_name, illness_health_condition_diet_dislikes_value) VALUES (%s, %s, %s)", (uid, new_cat, new_val))
  162. conn.commit()
  163. st.rerun()
  164. if eav_data:
  165. st.markdown("#### Active Flags")
  166. for e in eav_data:
  167. col1, col2 = st.columns([4, 1])
  168. col1.info(f"**{e['name']}:** {e['value'].title()}")
  169. if col2.button("X", key=f"del_eav_{e['id']}"):
  170. conn = get_db_connection('app_auth')
  171. with conn.cursor() as c:
  172. c.execute("DELETE FROM user_health_profiles WHERE id = %s", (e['id'],))
  173. conn.commit()
  174. st.rerun()
  175. else:
  176. tab1, tab2, tab3 = st.tabs(["Login", "Register", "Reset"])
  177. with tab1:
  178. l_user = st.text_input("Username", key="l_user")
  179. l_pass = st.text_input("Password", type="password", key="l_pass")
  180. if st.button("Login"):
  181. if verify_login(l_user, l_pass):
  182. st.session_state["authenticated_user"] = l_user
  183. st.rerun()
  184. else: st.error("Invalid login.")
  185. with tab2:
  186. r_user = st.text_input("Username", key="r_user")
  187. r_email = st.text_input("Email Address", key="r_email")
  188. r_pass = st.text_input("Password", type="password", key="r_pass")
  189. if st.button("Register"):
  190. if len(r_pass) < 6: st.error("Password too short.")
  191. elif register_user(r_user, r_pass, r_email): st.success("Registered safely!")
  192. else: st.error("Username exists.")
  193. with tab3:
  194. f_user = st.text_input("Username", key="f_user")
  195. f_email = st.text_input("Registered Email", key="f_email")
  196. if st.button("Send Reset Link"):
  197. if reset_password(f_user, f_email): st.success("Password reset emailed.")
  198. else: st.error("Failed.")
  199. if not st.session_state["authenticated_user"]:
  200. st.title("🍔 Food AI Medical Explorer")
  201. st.info("Please login to interrogate the Clinical Data.")
  202. st.stop()
  203. st.title("🍔 Food AI Clinical Explorer")
  204. conn_reader = get_db_connection('app_reader')
  205. tab_chat, tab_explore, tab_plate, tab_planner = st.tabs(["💬 AI Chat", "🔬 Clinical Search", "🍽️ My Plate Builder", "🤖 AI Meal Planner"])
  206. with tab_chat:
  207. st.subheader("Chat with the Context")
  208. if "messages" not in st.session_state:
  209. st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
  210. for msg in st.session_state.messages:
  211. st.chat_message(msg["role"]).write(msg["content"])
  212. if prompt := st.chat_input("Ask about the food items..."):
  213. st.session_state.messages.append({"role": "user", "content": prompt})
  214. st.chat_message("user").write(prompt)
  215. sys_prompt = "You are a helpful data analyst AI. Answer strictly using local data contexts. If you need external data, use the local_web_search tool!"
  216. with st.spinner("Analyzing..."):
  217. try:
  218. temp_messages = [{"role": "system", "content": sys_prompt}] + [m for m in st.session_state.messages if m["role"] != "tool"]
  219. response = ollama.chat(model='mistral', messages=temp_messages, tools=[search_tool_schema])
  220. if response.get('message', {}).get('tool_calls'):
  221. for tool in response['message']['tool_calls']:
  222. if tool['function']['name'] == 'local_web_search':
  223. query_arg = tool['function']['arguments'].get('query')
  224. st.info(f"🔍 Web Search triggered for: '{query_arg}'")
  225. search_data = local_web_search(query_arg)
  226. st.session_state.messages.append(response['message'])
  227. st.session_state.messages.append({'role': 'tool', 'content': search_data, 'name': 'local_web_search'})
  228. temp_messages = [{"role": "system", "content": sys_prompt}] + st.session_state.messages
  229. response = ollama.chat(model='mistral', messages=temp_messages)
  230. ai_reply = response['message']['content']
  231. except Exception as e: ai_reply = f"Hold on! Engine execution fault: {e}"
  232. st.session_state.messages.append({"role": "assistant", "content": ai_reply})
  233. st.chat_message("assistant").write(ai_reply)
  234. def highlight_medical_warnings(row):
  235. if '⚠️' in str(row.get('Medical Warning', '')): return ['background-color: rgba(255, 0, 0, 0.4); color: white;'] * len(row)
  236. return [''] * len(row)
  237. with tab_explore:
  238. st.subheader("Clinical Data Search")
  239. sq = st.text_input("Search Product Name or Ingredient")
  240. cols = st.columns(5)
  241. min_pro = cols[0].number_input("Min Protein (g)", 0, 1000, 0)
  242. min_fat = cols[1].number_input("Min Fat (g)", 0, 1000, 0)
  243. min_carb = cols[2].number_input("Min Carbs (g)", 0, 1000, 0)
  244. max_sug = cols[3].number_input("Max Sugar (g)", 0, 1000, 1000)
  245. # Load dynamically fetched limit as index
  246. opts = [10, 20, 50, 100, "All"]
  247. user_lim_str = get_user_limit(st.session_state["authenticated_user"])
  248. user_lim_val = "All" if user_lim_str == "All" else int(user_lim_str)
  249. idx = opts.index(user_lim_val) if user_lim_val in opts else 2
  250. limit_rc = cols[4].selectbox("Limit Results", opts, index=idx)
  251. if st.button("Search Database") and sq and conn_reader:
  252. with st.spinner("Processing massive clinical query..."):
  253. try:
  254. with conn_reader.cursor() as cursor:
  255. l_str = "" if limit_rc == "All" else f"LIMIT {limit_rc}"
  256. query = f"""
  257. SELECT code, product_name, generic_name, brands, allergens, ingredients_text,
  258. proteins_100g, fat_100g, carbohydrates_100g, sugars_100g, sodium_100g, energy_kcal_100g
  259. FROM products
  260. WHERE MATCH(product_name, ingredients_text) AGAINST(%s IN NATURAL LANGUAGE MODE)
  261. AND (proteins_100g >= %s OR proteins_100g IS NULL)
  262. AND (fat_100g >= %s OR fat_100g IS NULL)
  263. AND (carbohydrates_100g >= %s OR carbohydrates_100g IS NULL)
  264. AND (sugars_100g <= %s OR sugars_100g IS NULL)
  265. {l_str}
  266. """
  267. cursor.execute(query, (sq, min_pro, min_fat, min_carb, max_sug))
  268. results = cursor.fetchall()
  269. if results:
  270. # Fetch EAV Medical Profile
  271. eav_profile = get_eav_profile(st.session_state["authenticated_user"])
  272. df = pd.DataFrame(results)
  273. warnings_col = []
  274. for idx, row in df.iterrows():
  275. warns = []
  276. ing_text = str(row['ingredients_text']).lower()
  277. all_text = str(row['allergens']).lower()
  278. for param in eav_profile:
  279. cat = param['name'].lower()
  280. val = param['value']
  281. # Disease Analytics
  282. if cat == 'illness':
  283. if val == 'diabetes' and pd.notnull(row['sugars_100g']) and float(row['sugars_100g']) > 10.0:
  284. warns.append("⚠️ High Sugar (Diabetes)")
  285. if (val == 'hypertension' or val == 'high bp') and pd.notnull(row['sodium_100g']) and float(row['sodium_100g']) > 1.5:
  286. warns.append("⚠️ High Salt (Hypertension)")
  287. # Condition Analytics
  288. if cat == 'condition':
  289. if val == 'pregnant' and ('cru' in ing_text or 'raw' in ing_text or 'viande crue' in ing_text):
  290. warns.append("⚠️ Raw Foods (Pregnancy Toxoplasmosis)")
  291. if val == 'low fat' and pd.notnull(row['fat_100g']) and float(row['fat_100g']) > 20.0:
  292. warns.append("⚠️ High Fat")
  293. # Dietary Analytics (Best-Effort Keyword Filters)
  294. if cat == 'diet':
  295. if val in ['vegan', 'kosher', 'halal']:
  296. if val not in ing_text:
  297. warns.append(f"⚠️ Cannot verify {val.title()} compliance. Please check manual label.")
  298. if val == 'vegan' and ('lait' in ing_text or 'milk' in ing_text or 'oeuf' in ing_text or 'egg' in ing_text or 'meat' in ing_text or 'viande' in ing_text):
  299. warns.append("⚠️ Contains Animal Products (Not Vegan)")
  300. if val == 'halal' and ('porc' in ing_text or 'gelatin' in ing_text or 'vin' in ing_text or 'wine' in ing_text):
  301. warns.append("⚠️ Probable Haram Ingredients (e.g. Pork/Wine)")
  302. # Simple Exclusion List Analytics
  303. if cat in ['dislike', 'allergy']:
  304. if val in ing_text or val in all_text:
  305. warns.append(f"⚠️ Contains: {val.upper()}")
  306. warnings_col.append(" | ".join(list(set(warns))) if warns else "✅ Safe for Profile")
  307. df.insert(0, 'Medical Warning', warnings_col)
  308. styled_df = df.style.apply(highlight_medical_warnings, axis=1)
  309. st.success(f"Analysed {len(results)} records utilizing dynamic EAV parameters.")
  310. st.dataframe(styled_df, use_container_width=True)
  311. else:
  312. st.warning("No products found matching those strict terms.")
  313. except Exception as e: st.error(f"SQL/Pandas Error: {e}")
  314. with tab_plate:
  315. st.subheader("🍽️ My Plate Builder")
  316. uid = get_user_id(st.session_state["authenticated_user"])
  317. conn = get_db_connection('app_auth')
  318. if conn and uid:
  319. with conn.cursor() as cursor:
  320. cursor.execute("SELECT id, plate_name FROM plates WHERE user_id = %s", (uid,))
  321. plates = cursor.fetchall()
  322. with st.expander("➕ Create a New Plate"):
  323. new_plate_name = st.text_input("Plate Name")
  324. if st.button("Create Plate"):
  325. cursor.execute("INSERT INTO plates (user_id, plate_name) VALUES (%s, %s)", (uid, new_plate_name))
  326. conn.commit()
  327. st.rerun()
  328. if plates:
  329. selected_plate = st.selectbox("Select Active Plate", [p['plate_name'] for p in plates])
  330. active_p_id = next(p['id'] for p in plates if p['plate_name'] == selected_plate)
  331. cursor.execute("""
  332. SELECT i.id, i.product_code, i.quantity_grams, p.product_name, p.proteins_100g, p.fat_100g, p.carbohydrates_100g
  333. FROM plate_items i LEFT JOIN products p ON i.product_code = p.code WHERE i.plate_id = %s
  334. """, (active_p_id,))
  335. items = cursor.fetchall()
  336. if items:
  337. st.dataframe(items, use_container_width=True)
  338. total_pro = sum((float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
  339. total_fat = sum((float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
  340. total_carb = sum((float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
  341. st.info(f"**Total Protein:** {total_pro:.1f}g | **Total Fat:** {total_fat:.1f}g | **Total Carbs:** {total_carb:.1f}g")
  342. st.markdown("---")
  343. add_code = st.text_input("Enter exact Product `code`")
  344. add_grams = st.number_input("Portion Quantity (Grams)", min_value=1.0, value=100.0)
  345. if st.button("Add Item"):
  346. cursor.execute("INSERT INTO plate_items (plate_id, product_code, quantity_grams) VALUES (%s, %s, %s)",
  347. (active_p_id, add_code, add_grams))
  348. conn.commit()
  349. st.rerun()
  350. with tab_planner:
  351. st.subheader("🤖 AI Meal Planner")
  352. p_col1, p_col2, p_col3 = st.columns(3)
  353. target_cal = p_col1.number_input("Target Daily Calories (kcal)", 1000, 5000, 2000, 50)
  354. diet_pref = p_col2.selectbox("Dietary Preference", ["Omnivore", "Vegetarian", "Vegan", "Keto", "Paleo"])
  355. meal_count = p_col3.slider("Number of Meals", 2, 6, 3)
  356. extra_notes = st.text_input("Any additional allergies or goals?")
  357. if st.button("Generate Professional Menu"):
  358. with st.spinner("AI is formulating..."):
  359. sys_prompt = f"Dietitian planner. {diet_pref}, {target_cal}kcal, {meal_count} meals. Notes: {extra_notes}. OUTPUT AS STRICT MARKDOWN TABLE."
  360. response = ollama.chat(model='mistral', messages=[{'role': 'system', 'content': sys_prompt}, {'role': 'user', 'content': 'Generate menu'}])
  361. st.markdown(response['message']['content'])
  362. if conn_reader: conn_reader.close()