Просмотр исходного кода

Phase 2 Bug Fixes: Plate logic, LLM json format, chat tool leaks

lanfr144 2 недель назад
Родитель
Сommit
6d1e6da592
1 измененных файлов с 89 добавлено и 35 удалено
  1. 89 35
      app.py

+ 89 - 35
app.py

@@ -178,6 +178,13 @@ def reset_password(username, email):
     return False
     return False
 
 
 # UI Theming
 # UI Theming
+def render_version():
+    import os, datetime
+    file_time = datetime.datetime.fromtimestamp(os.path.getmtime(__file__)).strftime('%Y-%m-%d %H:%M:%S')
+    st.markdown("---")
+    st.caption("🚀 Version: v1.3.0")
+    st.caption(f"📅 Last Updated: {file_time}")
+
 st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
 st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
 st.markdown("""
 st.markdown("""
 <style>
 <style>
@@ -240,6 +247,7 @@ with st.sidebar:
                         c.execute("DELETE FROM user_health_profiles WHERE id = %s", (e['id'],))
                         c.execute("DELETE FROM user_health_profiles WHERE id = %s", (e['id'],))
                         conn.commit()
                         conn.commit()
                     st.rerun()
                     st.rerun()
+        render_version()
     else:
     else:
         tab1, tab2, tab3 = st.tabs(["Login", "Register", "Reset"])
         tab1, tab2, tab3 = st.tabs(["Login", "Register", "Reset"])
         with tab1:
         with tab1:
@@ -270,13 +278,7 @@ with st.sidebar:
                     st.success("Password reset emailed.")
                     st.success("Password reset emailed.")
                 else: 
                 else: 
                     st.error(f"Failed: {status}")
                     st.error(f"Failed: {status}")
-    
-    st.markdown("---")
-    st.caption("🚀 Version: v1.2.0")
-    
-    import os, datetime
-    file_time = datetime.datetime.fromtimestamp(os.path.getmtime(__file__)).strftime('%Y-%m-%d %H:%M:%S')
-    st.caption(f"📅 Last Updated: {file_time}")
+        render_version()
 
 
 if not st.session_state["authenticated_user"]:
 if not st.session_state["authenticated_user"]:
     st.title("🍔 Food AI Medical Explorer")
     st.title("🍔 Food AI Medical Explorer")
@@ -288,8 +290,14 @@ conn_reader = get_db_connection('app_reader')
 
 
 tab_chat, tab_explore, tab_plate, tab_planner = st.tabs(["💬 AI Chat", "🔬 Clinical Search", "🍽️ My Plate Builder", "🤖 AI Meal Planner"])
 tab_chat, tab_explore, tab_plate, tab_planner = st.tabs(["💬 AI Chat", "🔬 Clinical Search", "🍽️ My Plate Builder", "🤖 AI Meal Planner"])
 
 
+import re
+
 with tab_chat:
 with tab_chat:
-    st.subheader("Chat with the Context")
+    c1, c2 = st.columns([4, 1])
+    c1.subheader("Chat with the Context")
+    if c2.button("🧹 Clear Chat"):
+        st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
+        st.rerun()
     with st.expander("ℹ️ How to use this feature (Examples)"):
     with st.expander("ℹ️ How to use this feature (Examples)"):
         st.markdown("""
         st.markdown("""
         **Your active conditions (e.g. Pregnant, Diabetic) are automatically sent to the AI in the background. You do not need to type them out.**
         **Your active conditions (e.g. Pregnant, Diabetic) are automatically sent to the AI in the background. You do not need to type them out.**
@@ -304,12 +312,15 @@ with tab_chat:
     if "messages" not in st.session_state:
     if "messages" not in st.session_state:
         st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
         st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
 
 
-    for msg in st.session_state.messages:
-        st.chat_message(msg["role"]).write(msg["content"])
-
-    if prompt := st.chat_input("Ask about the food items..."):
         st.session_state.messages.append({"role": "user", "content": prompt})
         st.session_state.messages.append({"role": "user", "content": prompt})
-        st.chat_message("user").write(prompt)
+        
+        # Display chat history, filtering out TOOL_CALLS
+        for msg in st.session_state.messages:
+            if msg["role"] == "tool": continue
+            display_text = re.sub(r'\[TOOL_CALLS\]\s*\[.*?\]', '', msg["content"]).strip()
+            if display_text:
+                st.chat_message(msg["role"]).write(display_text)
+        
         user_eav = get_eav_profile(st.session_state["authenticated_user"])
         user_eav = get_eav_profile(st.session_state["authenticated_user"])
         profile_text = ", ".join([f"{p['name']}: {p['value']}" for p in user_eav]) if user_eav else "None"
         profile_text = ", ".join([f"{p['name']}: {p['value']}" for p in user_eav]) if user_eav else "None"
         
         
@@ -341,6 +352,7 @@ with tab_chat:
                     temp_messages = [{"role": "system", "content": sys_prompt}] + st.session_state.messages
                     temp_messages = [{"role": "system", "content": sys_prompt}] + st.session_state.messages
                     response = ollama.chat(model='llama3', messages=temp_messages)
                     response = ollama.chat(model='llama3', messages=temp_messages)
                 ai_reply = response['message']['content']
                 ai_reply = response['message']['content']
+                ai_reply = re.sub(r'\[TOOL_CALLS\]\s*\[.*?\]', '', ai_reply).strip()
             except Exception as e: ai_reply = f"Hold on! Engine execution fault: {e}"
             except Exception as e: ai_reply = f"Hold on! Engine execution fault: {e}"
 
 
         st.session_state.messages.append({"role": "assistant", "content": ai_reply})
         st.session_state.messages.append({"role": "assistant", "content": ai_reply})
@@ -375,15 +387,11 @@ with tab_explore:
     max_sug = cols[3].number_input("Max Sugar (g)", 0, 1000, 1000)
     max_sug = cols[3].number_input("Max Sugar (g)", 0, 1000, 1000)
     
     
     # Load dynamically fetched limit to prevent Pandas Styler crash
     # Load dynamically fetched limit to prevent Pandas Styler crash
-    try:
-        max_cells = pd.options.styler.render.max_elements
-    except AttributeError:
-        max_cells = 262144
-    dynamic_max_rows = max_cells // 15  # Approx 15 columns
-    opts = [10, 20, 50, 100, dynamic_max_rows]
+    pd.set_option("styler.render.max_elements", 5000000)
+    opts = [10, 50, 100, 500, 1000]
     
     
     user_lim_str = get_user_limit(st.session_state["authenticated_user"])
     user_lim_str = get_user_limit(st.session_state["authenticated_user"])
-    user_lim_val = dynamic_max_rows if user_lim_str == "All" else int(user_lim_str)
+    user_lim_val = 1000 if user_lim_str == "All" else int(user_lim_str)
     if user_lim_val not in opts: user_lim_val = 50
     if user_lim_val not in opts: user_lim_val = 50
     idx = opts.index(user_lim_val)
     idx = opts.index(user_lim_val)
     limit_rc = cols[4].selectbox("Limit Results", opts, index=idx)
     limit_rc = cols[4].selectbox("Limit Results", opts, index=idx)
@@ -545,9 +553,14 @@ with tab_explore:
 
 
 with tab_plate:
 with tab_plate:
     st.subheader("🍽️ My Plate Builder")
     st.subheader("🍽️ My Plate Builder")
-    with st.expander("ℹ️ How to use this feature (Examples)"):
+    with st.expander("ℹ️ How to use this feature (Examples & Logic)"):
         st.markdown("""
         st.markdown("""
-        **Mix and match ingredients to see if the combined macros exceed your profile's limits.**
+        **Plate Builder Logic:**
+        1. Create a New Plate.
+        2. Search for exact food words (e.g. 'chicken', 'egg').
+        3. Add the food with a specific portion (e.g. '150g').
+        4. The system calculates the combined macros.
+        5. Use the 🗑️ buttons to delete incorrect items or entire plates.
         
         
         *Example Plates:*
         *Example Plates:*
         1. `150g White Rice` + `50g Chicken Breast` + `100g Green Beans`
         1. `150g White Rice` + `50g Chicken Breast` + `100g Green Beans`
@@ -572,19 +585,33 @@ with tab_plate:
                     st.rerun()
                     st.rerun()
 
 
             if plates:
             if plates:
+                colA, colB = st.columns([4, 1])
                 plate_names = [p['plate_name'] for p in plates]
                 plate_names = [p['plate_name'] for p in plates]
                 default_idx = plate_names.index(st.session_state["active_plate"]) if "active_plate" in st.session_state and st.session_state["active_plate"] in plate_names else 0
                 default_idx = plate_names.index(st.session_state["active_plate"]) if "active_plate" in st.session_state and st.session_state["active_plate"] in plate_names else 0
-                selected_plate = st.selectbox("Select Active Plate", plate_names, index=default_idx)
+                selected_plate = colA.selectbox("Select Active Plate", plate_names, index=default_idx)
                 st.session_state["active_plate"] = selected_plate
                 st.session_state["active_plate"] = selected_plate
                 active_p_id = next(p['id'] for p in plates if p['plate_name'] == selected_plate)
                 active_p_id = next(p['id'] for p in plates if p['plate_name'] == selected_plate)
                 
                 
+                if colB.button("🗑️ Delete Plate"):
+                    cursor.execute("DELETE FROM plates WHERE id = %s", (active_p_id,))
+                    conn.commit()
+                    if "active_plate" in st.session_state: del st.session_state["active_plate"]
+                    st.rerun()
+                
                 cursor.execute("""
                 cursor.execute("""
                     SELECT i.id, i.product_code, i.quantity_grams, p.product_name, p.proteins_100g, p.fat_100g, p.carbohydrates_100g 
                     SELECT i.id, i.product_code, i.quantity_grams, p.product_name, p.proteins_100g, p.fat_100g, p.carbohydrates_100g 
                     FROM plate_items i LEFT JOIN products p ON i.product_code = p.code WHERE i.plate_id = %s
                     FROM plate_items i LEFT JOIN products p ON i.product_code = p.code WHERE i.plate_id = %s
                 """, (active_p_id,))
                 """, (active_p_id,))
                 items = cursor.fetchall()
                 items = cursor.fetchall()
                 if items:
                 if items:
-                    st.dataframe(items, use_container_width=True)
+                    for i in items:
+                        c1, c2 = st.columns([5, 1])
+                        c1.markdown(f"<li><b>{i['quantity_grams']}g</b> of {i['product_name']} (Pro: {i['proteins_100g'] or 0}g)</li>", unsafe_allow_html=True)
+                        if c2.button("🗑️", key=f"del_item_{i['id']}"):
+                            cursor.execute("DELETE FROM plate_items WHERE id = %s", (i['id'],))
+                            conn.commit()
+                            st.rerun()
+                            
                     total_pro = sum((float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
                     total_pro = sum((float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
                     total_fat = sum((float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
                     total_fat = sum((float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
                     total_carb = sum((float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
                     total_carb = sum((float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
@@ -592,17 +619,18 @@ with tab_plate:
                 
                 
                 st.markdown("---")
                 st.markdown("---")
                 st.markdown("#### ➕ Add Food to Plate")
                 st.markdown("#### ➕ Add Food to Plate")
-                add_search = st.text_input("Search Product Name")
+                add_search = st.text_input("Search Exact Product Name (e.g. 'chicken', 'egg')")
                 if add_search:
                 if add_search:
+                    bool_search = " ".join([f"+{w}" for w in add_search.split()])
                     cursor.execute("""
                     cursor.execute("""
                         SELECT c.code, c.product_name 
                         SELECT c.code, c.product_name 
                         FROM food_db.products_core c
                         FROM food_db.products_core c
                         JOIN food_db.products_macros m ON c.code = m.code
                         JOIN food_db.products_macros m ON c.code = m.code
-                        WHERE MATCH(c.product_name, c.ingredients_text) AGAINST(%s IN NATURAL LANGUAGE MODE)
+                        WHERE MATCH(c.product_name, c.ingredients_text) AGAINST(%s IN BOOLEAN MODE)
                         AND c.product_name IS NOT NULL AND c.product_name != ''
                         AND c.product_name IS NOT NULL AND c.product_name != ''
                         AND m.proteins_100g IS NOT NULL AND m.fat_100g IS NOT NULL AND m.carbohydrates_100g IS NOT NULL
                         AND m.proteins_100g IS NOT NULL AND m.fat_100g IS NOT NULL AND m.carbohydrates_100g IS NOT NULL
                         LIMIT 10
                         LIMIT 10
-                    """, (add_search,))
+                    """, (bool_search,))
                     search_res = cursor.fetchall()
                     search_res = cursor.fetchall()
                     if search_res:
                     if search_res:
                         options = {f"{r['product_name']} ({r['code']})": r for r in search_res}
                         options = {f"{r['product_name']} ({r['code']})": r for r in search_res}
@@ -655,13 +683,12 @@ with tab_planner:
             You MUST autonomously deduce what foods are recommended, forbidden, or accepted for these specific conditions and ensure the menu perfectly respects their medical requirements!
             You MUST autonomously deduce what foods are recommended, forbidden, or accepted for these specific conditions and ensure the menu perfectly respects their medical requirements!
             CRITICAL INSTRUCTIONS:
             CRITICAL INSTRUCTIONS:
             - YOU MUST USE the `search_nutrition_db` tool to find real products and their exact macros before constructing the menu!
             - YOU MUST USE the `search_nutrition_db` tool to find real products and their exact macros before constructing the menu!
-            - If you cannot find appropriate products in the local DB, use `local_web_search`.
-            - ALWAYS output exactly as a strict Markdown table including Columns: | Meal | Food | Calories | Salt (mg) | Fat (g) | Iron (mg) |
-            - DO NOT output | separated text outside of standard strict markdown block.
-            - Ensure the sum of the Calories EXACTLY matches the Target Daily Calories ({target_cal}).
-            - You MUST append a final row at the bottom of the table named "Total" that mathematically sums up the Calories, Salt, Fat, and Iron columns.
-            - Convert ALL cooking measurements to Grams (g). Use these equivalents STRICTLY:
-              1 tbsp = 15g, 1 tsp = 5g, 1 cup = 200g, 1 mustard glass = 100g. 1 cl of liquid = 10g.
+            - ALWAYS output exactly as a JSON array of objects. DO NOT OUTPUT MARKDOWN. DO NOT OUTPUT ANY TEXT EXCEPT JSON.
+            - JSON Format required:
+            [
+                {{"meal": "Breakfast", "food": "100g Oatmeal with 50g berries", "calories": 300, "salt_mg": 10, "fat_g": 5, "iron_mg": 2}}
+            ]
+            - Ensure the total calories sum up closely to {target_cal}.
             """
             """
             
             
             temp_messages = [{'role': 'system', 'content': sys_prompt}, {'role': 'user', 'content': 'Generate my meal plan. Find real foods from the DB.'}]
             temp_messages = [{'role': 'system', 'content': sys_prompt}, {'role': 'user', 'content': 'Generate my meal plan. Find real foods from the DB.'}]
@@ -687,6 +714,33 @@ with tab_planner:
                 else:
                 else:
                     break
                     break
                     
                     
-            st.markdown(response['message']['content'])
+            import json
+            raw_text = response['message']['content']
+            raw_text = re.sub(r'\[TOOL_CALLS\]\s*\[.*?\]', '', raw_text).strip()
+            
+            try:
+                start_idx = raw_text.find('[')
+                end_idx = raw_text.rfind(']') + 1
+                if start_idx != -1 and end_idx != -1:
+                    json_data = json.loads(raw_text[start_idx:end_idx])
+                    df_plan = pd.DataFrame(json_data)
+                    
+                    total_cals = df_plan['calories'].sum() if 'calories' in df_plan else 0
+                    total_salt = df_plan['salt_mg'].sum() if 'salt_mg' in df_plan else 0
+                    total_fat = df_plan['fat_g'].sum() if 'fat_g' in df_plan else 0
+                    total_iron = df_plan['iron_mg'].sum() if 'iron_mg' in df_plan else 0
+                    
+                    total_row = pd.DataFrame([{"meal": "TOTAL", "food": "---", "calories": total_cals, "salt_mg": total_salt, "fat_g": total_fat, "iron_mg": total_iron}])
+                    df_plan = pd.concat([df_plan, total_row], ignore_index=True)
+                    
+                    st.dataframe(df_plan, use_container_width=True)
+                    if abs(total_cals - target_cal) > 200:
+                        st.warning(f"Note: Total calories ({total_cals}) differ from your target ({target_cal}).")
+                else:
+                    st.error("AI failed to output valid JSON. Raw output:")
+                    st.text(raw_text)
+            except Exception as e:
+                st.error(f"Failed to parse AI output: {e}")
+                st.text(raw_text)
 
 
 if conn_reader: conn_reader.close()
 if conn_reader: conn_reader.close()