Browse Source

TG-154: Phase 3 Overhaul - Fix pandas limit, Plate Builder, Chat, Meal Planner JSON

lanfr144 2 tuần trước cách đây
mục cha
commit
8cfa56c45a
7 tập tin đã thay đổi với 183 bổ sung59 xóa
  1. 59 58
      app.py
  2. 15 0
      docs/Wiki_Home.md
  3. 27 0
      taiga_sync_fixes.py
  4. 32 0
      taiga_wiki_bookmarks.py
  5. 4 1
      taiga_wiki_push.py
  6. 39 0
      taiga_wiki_rename.py
  7. 7 0
      wiki_links_test.py

+ 59 - 58
app.py

@@ -43,11 +43,12 @@ def search_nutrition_db(query: str) -> str:
                 SELECT c.product_name, m.proteins_100g, m.fat_100g, m.carbohydrates_100g, m.sugars_100g 
                 FROM food_db.products_core c
                 LEFT 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)
-                AND c.product_name IS NOT NULL AND c.product_name != ''
+                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 != 'None'
                 LIMIT 5
             """
-            cursor.execute(sql, (query,))
+            bool_query = " ".join([f"+{w}" for w in query.split()])
+            cursor.execute(sql, (bool_query,))
             results = cursor.fetchall()
             if not results: return f"No database records found for '{query}'."
             
@@ -203,6 +204,7 @@ if "authenticated_user" not in st.session_state:
 
 with st.sidebar:
     st.title("User Portal 🔐")
+    render_version()
     if st.session_state["authenticated_user"]:
         st.success(f"Logged in as: {st.session_state['authenticated_user']}")
         if st.button("Logout"):
@@ -247,7 +249,6 @@ with st.sidebar:
                         c.execute("DELETE FROM user_health_profiles WHERE id = %s", (e['id'],))
                         conn.commit()
                     st.rerun()
-        render_version()
     else:
         tab1, tab2, tab3 = st.tabs(["Login", "Register", "Reset"])
         with tab1:
@@ -278,7 +279,6 @@ with st.sidebar:
                     st.success("Password reset emailed.")
                 else: 
                     st.error(f"Failed: {status}")
-        render_version()
 
 if not st.session_state["authenticated_user"]:
     st.title("🍔 Food AI Medical Explorer")
@@ -298,17 +298,17 @@ with tab_chat:
     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)"):
-        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.**
-        
-        *Examples:*
-        1. "I am pregnant, diabetic, and have kidney problems. Can I eat sushi?"
-        2. "What is a safe snack to stabilize my blood sugar without hurting my kidneys?"
-        3. "Can I drink milk? I need calcium for the baby."
-        4. "Is it safe to eat a large steak for iron?"
-        5. "What foods are strictly forbidden for me?"
-        """)
+    st.info("""
+    ℹ️ **How to use this feature (Examples)**
+    **Your active conditions (e.g. Pregnant, Diabetic) are automatically sent to the AI in the background. You do not need to type them out.**
+    
+    *Examples:*
+    1. "I am pregnant, diabetic, and have kidney problems. Can I eat sushi?"
+    2. "What is a safe snack to stabilize my blood sugar without hurting my kidneys?"
+    3. "Can I drink milk? I need calcium for the baby."
+    4. "Is it safe to eat a large steak for iron?"
+    5. "What foods are strictly forbidden for me?"
+    """)
     if "messages" not in st.session_state:
         st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
 
@@ -368,17 +368,17 @@ def highlight_medical_warnings(row):
 
 with tab_explore:
     st.subheader("Clinical Data Search")
-    with st.expander("ℹ️ How to use this feature (Examples)"):
-        st.markdown("""
-        **Your active conditions are automatically flagged (⚠️ or 💚) in the search results.**
-        
-        *Example Searches:*
-        1. `Cereal` *(Checks for high sugar & hidden phosphorus)*
-        2. `Cheese` *(Checks for unpasteurized pregnancy risks & high sodium)*
-        3. `Fruit Juice` *(Checks for high sugar spikes)*
-        4. `Deli Meat` *(Checks for Listeria risk & extreme sodium)*
-        5. `White Rice` *(Safe for kidneys but flags high glycemic index)*
-        """)
+    st.info("""
+    ℹ️ **How to use this feature (Examples)**
+    **Your active conditions are automatically flagged (⚠️ or 💚) in the search results.**
+    
+    *Example Searches:*
+    1. `Cereal` *(Checks for high sugar & hidden phosphorus)*
+    2. `Cheese` *(Checks for unpasteurized pregnancy risks & high sodium)*
+    3. `Fruit Juice` *(Checks for high sugar spikes)*
+    4. `Deli Meat` *(Checks for Listeria risk & extreme sodium)*
+    5. `White Rice` *(Safe for kidneys but flags high glycemic index)*
+    """)
     sq = st.text_input("Search Product Name or Ingredient")
     cols = st.columns(5)
     min_pro = cols[0].number_input("Min Protein (g)", 0, 1000, 0)
@@ -413,15 +413,16 @@ with tab_explore:
                         LEFT JOIN food_db.products_macros m ON c.code = m.code
                         LEFT JOIN food_db.products_vitamins v ON c.code = v.code
                         LEFT JOIN food_db.products_minerals min ON c.code = min.code
-                        WHERE MATCH(c.product_name, c.ingredients_text) AGAINST(%s IN NATURAL LANGUAGE MODE)
-                        AND c.product_name IS NOT NULL AND c.product_name != ''
+                        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 != 'None'
                         AND (m.proteins_100g >= %s OR m.proteins_100g IS NULL)
                         AND (m.fat_100g >= %s OR m.fat_100g IS NULL)
                         AND (m.carbohydrates_100g >= %s OR m.carbohydrates_100g IS NULL)
                         AND (m.sugars_100g <= %s OR m.sugars_100g IS NULL)
                         {l_str}
                     """
-                    cursor.execute(query, (sq, min_pro, min_fat, min_carb, max_sug))
+                    sq_bool = " ".join([f"+{w}" for w in sq.split()])
+                    cursor.execute(query, (sq_bool, min_pro, min_fat, min_carb, max_sug))
                     results = cursor.fetchall()
                     
                     if results:
@@ -553,22 +554,22 @@ with tab_explore:
 
 with tab_plate:
     st.subheader("🍽️ My Plate Builder")
-    with st.expander("ℹ️ How to use this feature (Examples & Logic)"):
-        st.markdown("""
-        **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:*
-        1. `150g White Rice` + `50g Chicken Breast` + `100g Green Beans`
-        2. `200g Potatoes` + `100g Tomatoes` + `100g Beef`
-        3. `100g Spinach Salad` + `50g Feta Cheese`
-        4. `200g Lentils` + `100g Quinoa`
-        5. `100g Apple` + `30g Almonds`
-        """)
+    st.info("""
+    ℹ️ **How to use this feature (Examples & Logic)**
+    **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:*
+    1. `150g White Rice` + `50g Chicken Breast` + `100g Green Beans`
+    2. `200g Potatoes` + `100g Tomatoes` + `100g Beef`
+    3. `100g Spinach Salad` + `50g Feta Cheese`
+    4. `200g Lentils` + `100g Quinoa`
+    5. `100g Apple` + `30g Almonds`
+    """)
     uid = get_user_id(st.session_state["authenticated_user"])
     conn = get_db_connection('app_auth')
     if conn and uid:
@@ -627,7 +628,7 @@ with tab_plate:
                         FROM food_db.products_core c
                         JOIN food_db.products_macros m ON c.code = m.code
                         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 c.product_name != 'None'
                         AND m.proteins_100g IS NOT NULL AND m.fat_100g IS NOT NULL AND m.carbohydrates_100g IS NOT NULL
                         LIMIT 10
                     """, (bool_search,))
@@ -655,17 +656,17 @@ with tab_plate:
 
 with tab_planner:
     st.subheader("🤖 AI Meal Planner")
-    with st.expander("ℹ️ How to use this feature (Examples)"):
-        st.markdown("""
-        **Your active conditions are automatically applied to the generated menu.**
-        
-        *Example Prompts:*
-        1. "Generate a full day meal plan for me. I am pregnant, diabetic, and have kidney disease."
-        2. "Plan a pregnancy-safe dinner that won't spike my blood sugar."
-        3. "I need a high-iron lunch that is safe for my kidneys."
-        4. "Plan a breakfast without dairy that is kidney-friendly."
-        5. "Give me a 3-day meal prep plan ensuring no raw fish, controlled protein portions, and steady complex carbs."
-        """)
+    st.info("""
+    ℹ️ **How to use this feature (Examples)**
+    **Your active conditions are automatically applied to the generated menu.**
+    
+    *Example Prompts:*
+    1. "Generate a full day meal plan for me. I am pregnant, diabetic, and have kidney disease."
+    2. "Plan a pregnancy-safe dinner that won't spike my blood sugar."
+    3. "I need a high-iron lunch that is safe for my kidneys."
+    4. "Plan a breakfast without dairy that is kidney-friendly."
+    5. "Give me a 3-day meal prep plan ensuring no raw fish, controlled protein portions, and steady complex carbs."
+    """)
     p_col1, p_col2, p_col3 = st.columns(3)
     target_cal = p_col1.number_input("Target Daily Calories (kcal)", 1000, 5000, 2000, 50)
     diet_pref = p_col2.selectbox("Dietary Preference", ["Omnivore", "Vegetarian", "Vegan", "Keto", "Paleo"])

+ 15 - 0
docs/Wiki_Home.md

@@ -0,0 +1,15 @@
+# Local Food AI Wiki
+
+Welcome to the Agile Scrum Wiki for the Local Food AI project.
+
+## Sprint 8 Documentation
+Please review the official documentation and Scrum Ritual logs below:
+
+- [[26-04-30-plan]] - Sprint Planning
+- [[26-04-30-daily]] - Daily Scrum (Stand-up)
+- [[26-04-30-review]] - Sprint Review
+- [[26-04-30-retrospective]] - Sprint Retrospective
+- [[26-04-30-artifact]] - Artifacts Used
+- [[wsl-deployment]] - WSL Deployment Playbook
+- [[backup-procedure]] - Server Backup Procedure
+- [[test-cases-sprint8]] - Clinical Verification Test Cases

+ 27 - 0
taiga_sync_fixes.py

@@ -0,0 +1,27 @@
+import requests, urllib3
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+base_url = 'https://192.168.130.161/taiga/api/v1'
+auth = requests.post(f'{base_url}/auth', json={'type': 'normal', 'username': 'FrancoisLange', 'password': 'BTSai123'}, verify=False).json()
+headers = {'Authorization': f'Bearer {auth["auth_token"]}', 'Content-Type': 'application/json'}
+proj_id = 21
+
+# Fetch sprint 8
+milestones = requests.get(f'{base_url}/milestones?project={proj_id}', headers=headers, verify=False).json()
+sprint8 = next((m for m in milestones if m['name'] == 'Sprint 8'), None)
+sprint_id = sprint8['id'] if sprint8 else None
+
+if sprint_id:
+    payload = {
+        "project": proj_id,
+        "subject": "Sprint 8 Final Bug Fixes & Polish",
+        "description": "Implemented dynamic Help Sections in UI, upgraded LLM to Llama3 with strict anti-hallucination prompts, dynamic Pandas Styler limit caps, MyPlate bug fixes, and null-macro product filters.",
+        "milestone": sprint_id
+    }
+    res = requests.post(f'{base_url}/userstories', json=payload, headers=headers, verify=False)
+    if res.status_code == 201:
+        us = res.json()
+        print(f"Created Fixes US: {us['subject']}")
+        t_payload = {"project": proj_id, "subject": "Execute Bug Fixes", "user_story": us['id'], "milestone": sprint_id}
+        requests.post(f'{base_url}/tasks', json=t_payload, headers=headers, verify=False)
+    else:
+        print(f"Failed US: {res.text}")

+ 32 - 0
taiga_wiki_bookmarks.py

@@ -0,0 +1,32 @@
+import requests, urllib3
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+base_url = 'https://192.168.130.161/taiga/api/v1'
+auth_url = f'{base_url}/auth'
+auth = requests.post(auth_url, json={'type': 'normal', 'username': 'FrancoisLange', 'password': 'BTSai123'}, verify=False).json()
+headers = {'Authorization': f'Bearer {auth["auth_token"]}', 'Content-Type': 'application/json'}
+proj_id = 21
+
+bookmarks = [
+    {"title": "26.04.30 PLAN (Sprint Planning)", "href": "26-04-30-plan"},
+    {"title": "26.04.30 DAILY (Daily Scrum)", "href": "26-04-30-daily"},
+    {"title": "26.04.30 REVIEW (Sprint Review)", "href": "26-04-30-review"},
+    {"title": "26.04.30 RETROSPECTIVE (Sprint Retrospective)", "href": "26-04-30-retrospective"},
+    {"title": "26.04.30 ARTIFACT (Artifacts Used)", "href": "26-04-30-artifact"}
+]
+
+# Get current links to calculate order
+existing = requests.get(f'{base_url}/wiki-links?project={proj_id}', headers=headers, verify=False).json()
+max_order = max([e['order'] for e in existing]) if existing else 0
+
+for i, b in enumerate(bookmarks):
+    payload = {
+        "project": proj_id,
+        "title": b["title"],
+        "href": b["href"],
+        "order": max_order + i + 1
+    }
+    r = requests.post(f'{base_url}/wiki-links', json=payload, headers=headers, verify=False)
+    print(f'Created Bookmark {b["title"]}: {r.status_code}')
+    if r.status_code != 201:
+        print(r.text)

+ 4 - 1
taiga_wiki_push.py

@@ -17,9 +17,11 @@ def push_wiki(slug, md_path):
     if len(res) > 0:
         wiki_id = res[0]['id']
         version = res[0]['version']
-        payload = {'content': content, 'version': version}
+        payload = {'content': content, 'version': version, 'project': proj_id, 'slug': slug}
         r = requests.put(f'{base_url}/wiki/{wiki_id}', json=payload, headers=headers, verify=False)
         print(f'Updated {slug}: {r.status_code}')
+        if r.status_code != 200:
+            print(r.text)
     else:
         payload = {'project': proj_id, 'slug': slug, 'content': content}
         r = requests.post(f'{base_url}/wiki', json=payload, headers=headers, verify=False)
@@ -28,6 +30,7 @@ def push_wiki(slug, md_path):
             print(r.text)
 
 # In Taiga, the home page of the wiki is usually 'home'
+push_wiki('home', 'docs/Wiki_Home.md')
 push_wiki('26-04-30-plan', 'docs/Scrum_Plan.md')
 push_wiki('26-04-30-daily', 'docs/Scrum_Daily.md')
 push_wiki('26-04-30-review', 'docs/Scrum_Review.md')

+ 39 - 0
taiga_wiki_rename.py

@@ -0,0 +1,39 @@
+import requests, urllib3
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+base_url = 'https://192.168.130.161/taiga/api/v1'
+auth_url = f'{base_url}/auth'
+auth = requests.post(auth_url, json={'type': 'normal', 'username': 'FrancoisLange', 'password': 'BTSai123'}, verify=False).json()
+headers = {'Authorization': f'Bearer {auth["auth_token"]}', 'Content-Type': 'application/json'}
+proj_id = 21
+
+# Fetch all existing links
+existing = requests.get(f'{base_url}/wiki-links?project={proj_id}', headers=headers, verify=False).json()
+
+# Delete the ones I made previously that had parentheses
+for link in existing:
+    if "26.04.30" in link['title']:
+        requests.delete(f'{base_url}/wiki-links/{link["id"]}', headers=headers, verify=False)
+        print(f"Deleted old link: {link['title']}")
+
+bookmarks = [
+    {"title": "26.04.30 PLAN", "href": "26-04-30-plan"},
+    {"title": "26.04.30 DAILY", "href": "26-04-30-daily"},
+    {"title": "26.04.30 REVIEW", "href": "26-04-30-review"},
+    {"title": "26.04.30 RETROSPECTIVE", "href": "26-04-30-retrospective"},
+    {"title": "26.04.30 ARTIFACT", "href": "26-04-30-artifact"}
+]
+
+# Calculate fresh order
+existing = requests.get(f'{base_url}/wiki-links?project={proj_id}', headers=headers, verify=False).json()
+max_order = max([e['order'] for e in existing]) if existing else 0
+
+for i, b in enumerate(bookmarks):
+    payload = {
+        "project": proj_id,
+        "title": b["title"],
+        "href": b["href"],
+        "order": max_order + i + 1
+    }
+    r = requests.post(f'{base_url}/wiki-links', json=payload, headers=headers, verify=False)
+    print(f'Created Bookmark {b["title"]}: {r.status_code}')

+ 7 - 0
wiki_links_test.py

@@ -0,0 +1,7 @@
+import requests, urllib3
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+base_url = 'https://192.168.130.161/taiga/api/v1'
+auth = requests.post(f'{base_url}/auth', json={'type': 'normal', 'username': 'FrancoisLange', 'password': 'BTSai123'}, verify=False).json()
+headers = {'Authorization': f'Bearer {auth["auth_token"]}', 'Content-Type': 'application/json'}
+res = requests.get(f'{base_url}/wiki-links?project=21', headers=headers, verify=False)
+print(res.status_code, res.text)