app.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import streamlit as st
  2. import pymysql
  3. import myloginpath
  4. import ollama
  5. import bcrypt
  6. # -------------------------------------------------------------------
  7. # Database Connections (PoLP & SoD)
  8. # -------------------------------------------------------------------
  9. def get_db_connection(login_path):
  10. """Dynamically connect using myloginpath to preserve Segregation of Duties."""
  11. try:
  12. conf = myloginpath.parse(login_path)
  13. return pymysql.connect(
  14. host=conf.get('host', '127.0.0.1'),
  15. user=conf.get('user'),
  16. password=conf.get('password'),
  17. database='food_db',
  18. cursorclass=pymysql.cursors.DictCursor
  19. )
  20. except Exception as e:
  21. st.error(f"Failed to connect using login-path '{login_path}'. Did you run mysql_config_editor?")
  22. st.sidebar.error(f"Connection Error: {e}")
  23. return None
  24. # -------------------------------------------------------------------
  25. # Authentication Logic
  26. # -------------------------------------------------------------------
  27. def verify_login(username, password):
  28. conn = get_db_connection('app_auth')
  29. if not conn: return False
  30. with conn.cursor() as cursor:
  31. cursor.execute("SELECT password_hash FROM users WHERE username = %s LIMIT 1", (username,))
  32. result = cursor.fetchone()
  33. conn.close()
  34. if result:
  35. # Check the hash
  36. return bcrypt.checkpw(password.encode('utf-8'), result['password_hash'].encode('utf-8'))
  37. return False
  38. def register_user(username, password):
  39. conn = get_db_connection('app_auth')
  40. if not conn: return False
  41. hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
  42. try:
  43. with conn.cursor() as cursor:
  44. cursor.execute("INSERT INTO users (username, password_hash) VALUES (%s, %s)", (username, hashed))
  45. conn.commit()
  46. conn.close()
  47. return True
  48. except pymysql.err.IntegrityError:
  49. return False # Username exists
  50. # -------------------------------------------------------------------
  51. # UI Flow
  52. # -------------------------------------------------------------------
  53. st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
  54. if "authenticated_user" not in st.session_state:
  55. st.session_state["authenticated_user"] = None
  56. # Sidebar Authentication
  57. with st.sidebar:
  58. st.title("User Portal 🔐")
  59. if st.session_state["authenticated_user"]:
  60. st.success(f"Logged in as: {st.session_state['authenticated_user']}")
  61. if st.button("Logout"):
  62. st.session_state["authenticated_user"] = None
  63. st.rerun()
  64. else:
  65. tab1, tab2 = st.tabs(["Login", "Register"])
  66. with tab1:
  67. l_user = st.text_input("Username", key="l_user")
  68. l_pass = st.text_input("Password", type="password", key="l_pass")
  69. if st.button("Login"):
  70. if verify_login(l_user, l_pass):
  71. st.session_state["authenticated_user"] = l_user
  72. st.success("Logged in successfully!")
  73. st.rerun()
  74. else:
  75. st.error("Invalid username or password.")
  76. with tab2:
  77. r_user = st.text_input("Username", key="r_user")
  78. r_pass = st.text_input("Password", type="password", key="r_pass")
  79. if st.button("Register"):
  80. if len(r_pass) < 6:
  81. st.error("Password too short.")
  82. elif register_user(r_user, r_pass):
  83. st.success("Registered successfully! Please log in.")
  84. else:
  85. st.error("Username already exists.")
  86. # Main Application Logic
  87. if not st.session_state["authenticated_user"]:
  88. st.title("🍔 Food AI Local Explorer")
  89. st.info("Please login or register on the sidebar to interact with the LLM.")
  90. st.stop() # Halt execution here, keeping it secure.
  91. # --- Authenticated App ---
  92. st.title("🍔 Food AI Local Explorer")
  93. st.markdown("Interrogate your database leveraging your private secure stack.")
  94. # Checking products via Reader Login path
  95. conn_reader = get_db_connection('app_reader')
  96. if conn_reader:
  97. with conn_reader.cursor() as cursor:
  98. cursor.execute("SELECT COUNT(*) as total FROM products;")
  99. total_products = cursor.fetchone()['total']
  100. st.sidebar.info(f"Database Scope: {total_products} products.")
  101. tab_chat, tab_explore = st.tabs(["💬 AI Chat", "🔍 Food Search & Details"])
  102. with tab_chat:
  103. st.subheader("Chat with the Context")
  104. if "messages" not in st.session_state:
  105. st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
  106. for msg in st.session_state.messages:
  107. st.chat_message(msg["role"]).write(msg["content"])
  108. if prompt := st.chat_input("Ask about the food items..."):
  109. st.session_state.messages.append({"role": "user", "content": prompt})
  110. st.chat_message("user").write(prompt)
  111. sys_prompt = "You are a helpful data analyst AI. Answer strictly using local data contexts."
  112. with st.spinner("Analyzing locally..."):
  113. try:
  114. response = ollama.chat(model='llama3', messages=[
  115. {'role': 'system', 'content': sys_prompt},
  116. {'role': 'user', 'content': prompt}
  117. ])
  118. ai_reply = response['message']['content']
  119. except Exception as e:
  120. ai_reply = f"Hold on! Could not reach Ollama Engine. Error: {e}"
  121. st.session_state.messages.append({"role": "assistant", "content": ai_reply})
  122. st.chat_message("assistant").write(ai_reply)
  123. with tab_explore:
  124. st.subheader("Raw Data Search")
  125. search_query = st.text_input("Search Product Name or Ingredient (e.g. 'Nutella' or 'Sugar')")
  126. if st.button("Search Database") and search_query and conn_reader:
  127. with st.spinner("Querying MySQL..."):
  128. try:
  129. with conn_reader.cursor() as cursor:
  130. # Leverage the FULLTEXT INDEX built in init.sql
  131. query = """
  132. SELECT code, product_name, generic_name, brands, ingredients_text
  133. FROM products
  134. WHERE MATCH(product_name, ingredients_text) AGAINST(%s IN NATURAL LANGUAGE MODE)
  135. LIMIT 50
  136. """
  137. cursor.execute(query, (search_query,))
  138. results = cursor.fetchall()
  139. except Exception as e:
  140. st.error(f"SQL Error: {e}")
  141. results = []
  142. if results:
  143. st.success(f"Found {len(results)} matching records!")
  144. st.dataframe(results, use_container_width=True)
  145. else:
  146. st.warning("No products found matching those terms.")
  147. if conn_reader:
  148. conn_reader.close()