Prechádzať zdrojové kódy

TG-5: Streamlit Application Base

lanfr144 4 týždňov pred
rodič
commit
64071f592d
1 zmenil súbory, kde vykonal 170 pridanie a 0 odobranie
  1. 170 0
      app.py

+ 170 - 0
app.py

@@ -0,0 +1,170 @@
+import streamlit as st
+import pymysql
+import myloginpath
+import ollama
+import bcrypt
+
+# -------------------------------------------------------------------
+# Database Connections (PoLP & SoD)
+# -------------------------------------------------------------------
+def get_db_connection(login_path):
+    """Dynamically connect using myloginpath to preserve Segregation of Duties."""
+    try:
+        conf = myloginpath.parse(login_path)
+        return pymysql.connect(
+            host=conf.get('host', '127.0.0.1'),
+            user=conf.get('user'),
+            password=conf.get('password'),
+            database='food_db',
+            cursorclass=pymysql.cursors.DictCursor
+        )
+    except Exception as e:
+        st.error(f"Failed to connect using login-path '{login_path}'. Did you run mysql_config_editor?")
+        st.sidebar.error(f"Connection Error: {e}")
+        return None
+
+# -------------------------------------------------------------------
+# Authentication Logic
+# -------------------------------------------------------------------
+def verify_login(username, password):
+    conn = get_db_connection('app_auth')
+    if not conn: return False
+    
+    with conn.cursor() as cursor:
+        cursor.execute("SELECT password_hash FROM users WHERE username = %s LIMIT 1", (username,))
+        result = cursor.fetchone()
+        conn.close()
+        
+        if result:
+            # Check the hash
+            return bcrypt.checkpw(password.encode('utf-8'), result['password_hash'].encode('utf-8'))
+    return False
+
+def register_user(username, password):
+    conn = get_db_connection('app_auth')
+    if not conn: return False
+    
+    hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
+    try:
+        with conn.cursor() as cursor:
+            cursor.execute("INSERT INTO users (username, password_hash) VALUES (%s, %s)", (username, hashed))
+            conn.commit()
+        conn.close()
+        return True
+    except pymysql.err.IntegrityError:
+        return False  # Username exists
+
+# -------------------------------------------------------------------
+# UI Flow
+# -------------------------------------------------------------------
+st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
+
+if "authenticated_user" not in st.session_state:
+    st.session_state["authenticated_user"] = None
+
+# Sidebar Authentication
+with st.sidebar:
+    st.title("User Portal 🔐")
+    if st.session_state["authenticated_user"]:
+        st.success(f"Logged in as: {st.session_state['authenticated_user']}")
+        if st.button("Logout"):
+            st.session_state["authenticated_user"] = None
+            st.rerun()
+    else:
+        tab1, tab2 = st.tabs(["Login", "Register"])
+        with tab1:
+            l_user = st.text_input("Username", key="l_user")
+            l_pass = st.text_input("Password", type="password", key="l_pass")
+            if st.button("Login"):
+                if verify_login(l_user, l_pass):
+                    st.session_state["authenticated_user"] = l_user
+                    st.success("Logged in successfully!")
+                    st.rerun()
+                else:
+                    st.error("Invalid username or password.")
+        with tab2:
+            r_user = st.text_input("Username", key="r_user")
+            r_pass = st.text_input("Password", type="password", key="r_pass")
+            if st.button("Register"):
+                if len(r_pass) < 6:
+                    st.error("Password too short.")
+                elif register_user(r_user, r_pass):
+                    st.success("Registered successfully! Please log in.")
+                else:
+                    st.error("Username already exists.")
+
+# Main Application Logic
+if not st.session_state["authenticated_user"]:
+    st.title("🍔 Food AI Local Explorer")
+    st.info("Please login or register on the sidebar to interact with the LLM.")
+    st.stop()  # Halt execution here, keeping it secure.
+
+# --- Authenticated App ---
+st.title("🍔 Food AI Local Explorer")
+st.markdown("Interrogate your database leveraging your private secure stack.")
+
+# Checking products via Reader Login path
+conn_reader = get_db_connection('app_reader')
+if conn_reader:
+    with conn_reader.cursor() as cursor:
+        cursor.execute("SELECT COUNT(*) as total FROM products;")
+        total_products = cursor.fetchone()['total']
+        st.sidebar.info(f"Database Scope: {total_products} products.")
+
+tab_chat, tab_explore = st.tabs(["💬 AI Chat", "🔍 Food Search & Details"])
+
+with tab_chat:
+    st.subheader("Chat with the Context")
+    if "messages" not in st.session_state:
+        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.chat_message("user").write(prompt)
+        sys_prompt = "You are a helpful data analyst AI. Answer strictly using local data contexts."
+        
+        with st.spinner("Analyzing locally..."):
+            try:
+                response = ollama.chat(model='llama3', messages=[
+                    {'role': 'system', 'content': sys_prompt},
+                    {'role': 'user', 'content': prompt}
+                ])
+                ai_reply = response['message']['content']
+            except Exception as e:
+                ai_reply = f"Hold on! Could not reach Ollama Engine. Error: {e}"
+
+        st.session_state.messages.append({"role": "assistant", "content": ai_reply})
+        st.chat_message("assistant").write(ai_reply)
+
+with tab_explore:
+    st.subheader("Raw Data Search")
+    search_query = st.text_input("Search Product Name or Ingredient (e.g. 'Nutella' or 'Sugar')")
+    
+    if st.button("Search Database") and search_query and conn_reader:
+        with st.spinner("Querying MySQL..."):
+            try:
+                with conn_reader.cursor() as cursor:
+                    # Leverage the FULLTEXT INDEX built in init.sql
+                    query = """
+                        SELECT code, product_name, generic_name, brands, ingredients_text 
+                        FROM products 
+                        WHERE MATCH(product_name, ingredients_text) AGAINST(%s IN NATURAL LANGUAGE MODE)
+                        LIMIT 50
+                    """
+                    cursor.execute(query, (search_query,))
+                    results = cursor.fetchall()
+            except Exception as e:
+                st.error(f"SQL Error: {e}")
+                results = []
+                
+        if results:
+            st.success(f"Found {len(results)} matching records!")
+            st.dataframe(results, use_container_width=True)
+        else:
+            st.warning("No products found matching those terms.")
+
+if conn_reader:
+    conn_reader.close()