Upload app.py
Browse files
    	
        app.py
    ADDED
    
    | @@ -0,0 +1,135 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            import sys
         | 
| 2 | 
            +
            import os
         | 
| 3 | 
            +
            import logging
         | 
| 4 | 
            +
            from typing import AsyncIterator
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            from agents import (Runner, set_default_openai_key, trace)
         | 
| 7 | 
            +
            from openai.types.responses import ResponseTextDeltaEvent
         | 
| 8 | 
            +
            import streamlit as st
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            from backend import lichtblick_agent
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            # ====================
         | 
| 13 | 
            +
            # Setup logging
         | 
| 14 | 
            +
            # ====================
         | 
| 15 | 
            +
            logging.basicConfig(
         | 
| 16 | 
            +
                level=logging.INFO,
         | 
| 17 | 
            +
                format="%(asctime)s | %(levelname)s | %(name)s | %(message)s"
         | 
| 18 | 
            +
            )
         | 
| 19 | 
            +
            logger = logging.getLogger("Lichtblick")
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            # ============================
         | 
| 22 | 
            +
            # Core async function
         | 
| 23 | 
            +
            # ============================
         | 
| 24 | 
            +
            async def llm_response(api_key: str, message: str) -> AsyncIterator[str]:
         | 
| 25 | 
            +
                """
         | 
| 26 | 
            +
                Streams the response from the Lichtblick assistant as an async text generator.
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                Args:
         | 
| 29 | 
            +
                    api_key (str): The OpenAI API key to authenticate requests.
         | 
| 30 | 
            +
                    message (str): The user's input message to be processed by the agent.
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                Yields:
         | 
| 33 | 
            +
                    str: Incremental chunks of the assistant's response text as they are streamed.
         | 
| 34 | 
            +
                """
         | 
| 35 | 
            +
                set_default_openai_key(api_key)
         | 
| 36 | 
            +
                if not api_key or not api_key.startswith("sk-"):
         | 
| 37 | 
            +
                    logger.error("Missing or invalid OpenAI API key.")
         | 
| 38 | 
            +
                    yield "🤖 API key is missing or invalid. Please check your .env file."
         | 
| 39 | 
            +
                    return
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                # Construct context from message history
         | 
| 42 | 
            +
                context = ""
         | 
| 43 | 
            +
                for msg in st.session_state.messages[-5:]:  # Use last 5 turns
         | 
| 44 | 
            +
                    role = "User" if msg["role"] == "user" else "Assistant"
         | 
| 45 | 
            +
                    content = msg["content"]
         | 
| 46 | 
            +
                    context += f"{role}: {content}\n\n"  # Double newline for clarity
         | 
| 47 | 
            +
                context += f"User: {message}"
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                try:
         | 
| 50 | 
            +
                    result = Runner.run_streamed(lichtblick_agent, input=message)
         | 
| 51 | 
            +
                    async for event in result.stream_events():
         | 
| 52 | 
            +
                        if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
         | 
| 53 | 
            +
                            if event.data.delta:
         | 
| 54 | 
            +
                                yield event.data.delta
         | 
| 55 | 
            +
                    logger.info("Agent streaming complete.")
         | 
| 56 | 
            +
                except Exception as e:
         | 
| 57 | 
            +
                    logger.exception("Error during agent processing.")
         | 
| 58 | 
            +
                    yield "🤖 Sorry, something went wrong. Please try again."
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            # ============================
         | 
| 61 | 
            +
            # Lichtblick Streamlit App
         | 
| 62 | 
            +
            # ============================
         | 
| 63 | 
            +
            st.set_page_config(
         | 
| 64 | 
            +
                page_title="Lichtblick", 
         | 
| 65 | 
            +
                page_icon="🇩🇪📚", 
         | 
| 66 | 
            +
                layout="centered", 
         | 
| 67 | 
            +
                initial_sidebar_state="collapsed"
         | 
| 68 | 
            +
            )
         | 
| 69 | 
            +
             | 
| 70 | 
            +
            # Sidebar
         | 
| 71 | 
            +
            with st.sidebar:
         | 
| 72 | 
            +
                st.image("assets/lichtblick_mascot.png")
         | 
| 73 | 
            +
                openai_api_key = st.sidebar.text_input("OpenAI API Key", type="password")
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                # ✅ Show "ready" toast only once per session
         | 
| 76 | 
            +
                if openai_api_key and openai_api_key.startswith("sk-"):
         | 
| 77 | 
            +
                    if not st.session_state.get("api_key_validated"):
         | 
| 78 | 
            +
                        st.toast("💡 Lichtblick is ready!", icon="✅")
         | 
| 79 | 
            +
                        st.session_state.api_key_validated = True
         | 
| 80 | 
            +
                elif openai_api_key:
         | 
| 81 | 
            +
                    st.toast("❌ Invalid API key format.", icon="⚠️")
         | 
| 82 | 
            +
                    st.session_state.api_key_validated = False
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                # Clear chat + reset session
         | 
| 85 | 
            +
                # 🧹 Centered Clear Chat Button in the sidebar
         | 
| 86 | 
            +
                st.markdown("<div style='text-align: center;'>", unsafe_allow_html=True)
         | 
| 87 | 
            +
                if st.button("🧹 Clear Chat", use_container_width=True):
         | 
| 88 | 
            +
                    st.session_state.messages = []
         | 
| 89 | 
            +
                    st.session_state.api_key_validated = False
         | 
| 90 | 
            +
                    st.toast("🧹 Chat history cleared!", icon="✅")
         | 
| 91 | 
            +
                st.markdown("</div>", unsafe_allow_html=True)
         | 
| 92 | 
            +
             | 
| 93 | 
            +
             | 
| 94 | 
            +
            # App title
         | 
| 95 | 
            +
            st.title("💡:blue[_Lichtblick_] :orange[_Assistant_]💡")
         | 
| 96 | 
            +
             | 
| 97 | 
            +
            # Short Description
         | 
| 98 | 
            +
            with st.expander("ℹ️ What is Lichtblick?"):
         | 
| 99 | 
            +
                st.markdown("Lichtblick is your smart and supportive assistant for learning German through sentence analysis, vocabulary help, and clear explanations.")
         | 
| 100 | 
            +
             | 
| 101 | 
            +
             | 
| 102 | 
            +
            # Initialize chat history
         | 
| 103 | 
            +
            if "messages" not in st.session_state:
         | 
| 104 | 
            +
                st.session_state.messages = []
         | 
| 105 | 
            +
             | 
| 106 | 
            +
            # Display chat messages from history
         | 
| 107 | 
            +
            for message in st.session_state.messages:
         | 
| 108 | 
            +
                with st.chat_message(message["role"]):
         | 
| 109 | 
            +
                    st.markdown(message["content"])
         | 
| 110 | 
            +
             | 
| 111 | 
            +
            # Accept user input
         | 
| 112 | 
            +
            if user_input := st.chat_input("Ready to learn German? Ask me anything!"):
         | 
| 113 | 
            +
                if not openai_api_key or not openai_api_key.startswith("sk-"):
         | 
| 114 | 
            +
                    st.toast("❌ Please enter a valid OpenAI API key.", icon="⚠️")
         | 
| 115 | 
            +
                    st.stop()
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                elif user_input.strip() == "":
         | 
| 118 | 
            +
                    st.toast("⚠️ Please enter a message.", icon="⚠️")
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                else:
         | 
| 121 | 
            +
                    # Add user message to chat history
         | 
| 122 | 
            +
                    st.session_state.messages.append({"role": "user", "content": user_input})
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    with st.chat_message("user", avatar="🤵🏻"):
         | 
| 125 | 
            +
                        st.markdown(user_input)
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                    with st.chat_message("assistant", avatar="🤖"):
         | 
| 128 | 
            +
                        try:
         | 
| 129 | 
            +
                            with st.spinner("💬 Lichtblick is thinking..."):
         | 
| 130 | 
            +
                                with trace("Lichtblick workflow"):
         | 
| 131 | 
            +
                                    response = st.write_stream(llm_response(api_key=openai_api_key, message=user_input))
         | 
| 132 | 
            +
                            st.session_state.messages.append({"role": "assistant", "content": response})
         | 
| 133 | 
            +
                        except Exception as e:
         | 
| 134 | 
            +
                            logger.exception("Exception in response streaming.")
         | 
| 135 | 
            +
                            st.toast("🤖 Oops! Something went wrong while processing your request.", icon="❌")
         | 
