├── demo_app ├── __init__.py ├── components │ ├── __init__.py │ ├── faq.py │ └── sidebar.py ├── main_simple.py ├── main_lg0.py └── main.py ├── poetry.toml ├── .streamlit └── config.toml ├── ui.PNG ├── requirements.txt ├── docker-compose.yml ├── pyproject.toml ├── .devcontainer ├── Dockerfile ├── buildWithRust.Dockerfile └── devcontainer.json ├── cloudbuild.yaml ├── Dockerfile ├── LICENSE ├── app.yaml ├── .gitignore ├── .gcloudignore └── README.md /demo_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo_app/components/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = false 3 | -------------------------------------------------------------------------------- /.streamlit/config.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | maxUploadSize = 15 3 | runOnSave = true -------------------------------------------------------------------------------- /ui.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amjadraza/langchain-streamlit-docker-template/HEAD/ui.PNG -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | langchain >= 1.1.3 2 | langchain-openai >= 1.1.2 3 | streamlit >= 1.52.1 4 | langchain-community >= 0.4.1 5 | pypdf>=6.4.1 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | langchain-chat-app: 4 | image: langchain-chat-app:latest 5 | build: . 6 | command: streamlit run demo_app/main.py --server.port 8080 7 | # volumes: 8 | # - ./demo_app/:/app/demo_app 9 | ports: 10 | - "8080:8080" 11 | environment: 12 | - HOST=0.0.0.0 13 | - LISTEN_PORT=8080 14 | restart: unless-stopped -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "langchain-streamlit-docker-template" 3 | version = "0.1.0" 4 | description = "A template langchain Streamlit App with Docker" 5 | authors = [ 6 | {name = "Amjad Raza", email = "amjad@example.com"} 7 | ] 8 | license = {text = "MIT"} 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | dependencies = [ 12 | "langchain>=1.1.3", 13 | "langchain-openai>=1.1.2", 14 | "langchain-community>=0.4.1", 15 | "streamlit>=1.52.1", 16 | "pypdf>=6.4.1", 17 | 18 | ] 19 | 20 | [build-system] 21 | requires = ["hatchling"] 22 | build-backend = "hatchling.build" -------------------------------------------------------------------------------- /demo_app/components/faq.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import streamlit as st 3 | 4 | 5 | def faq(): 6 | st.markdown( 7 | """ 8 | # FAQ 9 | ## How to use App Template? 10 | This is a basic template to set up Langchain Demo App with Docker 11 | 12 | 13 | ## What Libraries are being use? 14 | Basic Setup is using langchain, streamlit and openai. 15 | 16 | ## How to test the APP? 17 | Set up the OpenAI API keys and run the App 18 | 19 | ## Disclaimer? 20 | This is a template App, when using with openai_api key, you will be charged a nominal fee depending 21 | on number of prompts etc. 22 | 23 | """ 24 | ) 25 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim AS builder 2 | 3 | ENV CARGO_HOME="/opt/.cargo" 4 | 5 | SHELL [ "/bin/bash", "-o", "pipefail", "-c" ] 6 | 7 | WORKDIR /opt 8 | 9 | # The installer requires curl (and certificates) to download the release archive 10 | # hadolint ignore=DL3008 11 | RUN apt-get update && \ 12 | apt-get install -y --no-install-recommends ca-certificates curl 13 | 14 | # Run uv installer 15 | RUN curl -LsSf https://astral.sh/uv/install.sh | sh 16 | 17 | 18 | FROM mcr.microsoft.com/vscode/devcontainers/base:bookworm 19 | 20 | LABEL maintainer="a5chin " 21 | 22 | ENV CARGO_HOME="/opt/.cargo" 23 | ENV PATH="$CARGO_HOME/bin/:$PATH" 24 | ENV PYTHONUNBUFFERED=True 25 | ENV UV_LINK_MODE=copy 26 | 27 | WORKDIR /opt 28 | 29 | COPY --from=builder --chown=vscode: $CARGO_HOME $CARGO_HOME 30 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker' 3 | id: Build Image 4 | entrypoint: bash 5 | args: 6 | - -c 7 | - | 8 | DOCKER_BUILDKIT=1 docker build --target=runtime . -t australia-southeast1-docker.pkg.dev/langchain-chat/app/langchain-chat-app:latest \ 9 | && docker push australia-southeast1-docker.pkg.dev/langchain-chat/app/langchain-chat-app:latest 10 | 11 | - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' 12 | entrypoint: gcloud 13 | id: Deploy API 14 | args: ['run', 'deploy', 'langchain-chat', 15 | '--image=australia-southeast1-docker.pkg.dev/langchain-chat/app/langchain-chat-app:latest', 16 | '--region=australia-southeast1', '--service-account=langchain-app-cr@langchain-chat.iam.gserviceaccount.com', 17 | '--allow-unauthenticated', 18 | '--set-env-vars=STREAMLIT_SERVER_PORT=8080'] 19 | waitFor: [ 'Build Image' ] 20 | 21 | images: 22 | - australia-southeast1-docker.pkg.dev/langchain-chat/app/langchain-chat-app:latest -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # The builder image, used to build the virtual environment 2 | FROM python:3.12-slim-bookworm as builder 3 | COPY --from=ghcr.io/astral-sh/uv:0.4.24 /uv /uvx /bin/ 4 | 5 | RUN apt-get update && apt-get install -y git 6 | 7 | ENV HOST=0.0.0.0 8 | ENV LISTEN_PORT 8080 9 | EXPOSE 8080 10 | 11 | WORKDIR /app 12 | 13 | COPY pyproject.toml ./ 14 | 15 | # Install dependencies using uv 16 | RUN uv venv .venv 17 | RUN /bin/bash -c "source .venv/bin/activate && uv pip compile pyproject.toml > requirements.txt && uv pip install -r requirements.txt" 18 | 19 | # The runtime image, used to just run the code provided its virtual environment 20 | FROM python:3.12-slim-bookworm as runtime 21 | 22 | ENV VIRTUAL_ENV=/app/.venv \ 23 | PATH="/app/.venv/bin:$PATH" 24 | 25 | COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} 26 | COPY --from=builder /app/requirements.txt ./requirements.txt 27 | 28 | COPY ./demo_app ./demo_app 29 | COPY ./.streamlit ./.streamlit 30 | 31 | CMD ["streamlit", "run", "demo_app/main.py", "--server.port", "8080"] -------------------------------------------------------------------------------- /.devcontainer/buildWithRust.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim AS builder 2 | 3 | ENV CARGO_HOME="/opt/.cargo" 4 | ENV RUSTUP_HOME="/opt/.rustup" 5 | 6 | SHELL [ "/bin/bash", "-o", "pipefail", "-c" ] 7 | 8 | WORKDIR /opt 9 | 10 | # The installer requires curl (and certificates) to download the release archive 11 | # hadolint ignore=DL3008 12 | RUN apt-get update && \ 13 | apt-get install -y --no-install-recommends ca-certificates curl 14 | 15 | # Run uv and rustup installer 16 | RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ 17 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 18 | 19 | 20 | FROM mcr.microsoft.com/vscode/devcontainers/base:bookworm 21 | 22 | LABEL maintainer="a5chin " 23 | 24 | ENV CARGO_HOME="/opt/.cargo" 25 | ENV RUSTUP_HOME="/opt/.rustup" 26 | ENV PATH="$CARGO_HOME/bin/:$PATH" 27 | ENV PYTHONUNBUFFERED=True 28 | ENV UV_LINK_MODE=copy 29 | 30 | WORKDIR /opt 31 | 32 | COPY --from=builder --chown=vscode: $CARGO_HOME $CARGO_HOME 33 | COPY --from=builder $RUSTUP_HOME $RUSTUP_HOME 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 DR. AMJAD RAZA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | #--------------------------------------------------------------- 16 | #runtime: python 17 | #env: flex 18 | #entrypoint: streamlit run -b pandasai_app/main.py 19 | # 20 | #runtime_config: 21 | # operating_system: ubuntu22 22 | #--------------------------------------------------------------- 23 | 24 | # With Dockerfile 25 | runtime: custom 26 | env: flex 27 | 28 | # This sample incurs costs to run on the App Engine flexible environment. 29 | # The settings below are to reduce costs during testing and are not appropriate 30 | # for production use. For more information, see: 31 | # https://cloud.google.com/appengine/docs/flexible/python/configuring-your-app-with-app-yaml 32 | 33 | #network: 34 | # forwarded_ports: 35 | # - 8501/tcp 36 | 37 | manual_scaling: 38 | instances: 1 39 | 40 | resources: 41 | cpu: 1 42 | memory_gb: 0.5 43 | disk_size_gb: 10 -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uv", 3 | "build": { 4 | "context": "..", 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "features": { 8 | "ghcr.io/dhoeric/features/hadolint:1": {} 9 | }, 10 | "customizations": { 11 | "vscode": { 12 | "extensions": [ 13 | "charliermarsh.ruff", 14 | "codezombiech.gitignore", 15 | "eamodio.gitlens", 16 | "exiasr.hadolint", 17 | "kevinrose.vsc-python-indent", 18 | "mosapride.zenkaku", 19 | "ms-azuretools.vscode-docker", 20 | "ms-python.python", 21 | "njpwerner.autodocstring", 22 | "oderwat.indent-rainbow", 23 | "pkief.material-icon-theme", 24 | "redhat.vscode-yaml", 25 | "shardulm94.trailing-spaces", 26 | "tamasfe.even-better-toml", 27 | "usernamehw.errorlens", 28 | "yzhang.markdown-all-in-one" 29 | ], 30 | "settings": { 31 | "terminal.integrated.defaultProfile.linux": "zsh", 32 | "terminal.integrated.profiles.linux": { 33 | "zsh": { 34 | "path": "/bin/zsh" 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | "postCreateCommand": "uv python pin $(cat .python-version) && uv sync --dev", 41 | "postStartCommand": "uv run pre-commit install", 42 | "remoteUser": "vscode" 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local data 2 | data/local_data/ 3 | 4 | # Secrets 5 | .streamlit/secrets.toml 6 | 7 | # VSCode 8 | .vscode/ 9 | 10 | # TODO 11 | TODO.md 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | cover/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | .pybuilder/ 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pdm 98 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 99 | #pdm.lock 100 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 101 | # in version control. 102 | # https://pdm.fming.dev/#use-with-ide 103 | .pdm.toml 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # Cython debug symbols 146 | cython_debug/ 147 | 148 | # PyCharm 149 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 150 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 151 | # and can be added to the global gitignore or merged into this file. For a more nuclear 152 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 153 | .idea/ 154 | credentials.json 155 | token.json 156 | 157 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # Local data 2 | data/local_data/ 3 | 4 | # Secrets 5 | .streamlit/secrets.toml 6 | 7 | # VSCode 8 | .vscode/ 9 | 10 | # TODO 11 | TODO.md 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | cover/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | .pybuilder/ 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pdm 98 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 99 | #pdm.lock 100 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 101 | # in version control. 102 | # https://pdm.fming.dev/#use-with-ide 103 | .pdm.toml 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # Cython debug symbols 146 | cython_debug/ 147 | 148 | # PyCharm 149 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 150 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 151 | # and can be added to the global gitignore or merged into this file. For a more nuclear 152 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 153 | .idea/ 154 | .idea 155 | .gcloudignore 156 | credentials.json 157 | token.json 158 | 159 | -------------------------------------------------------------------------------- /demo_app/main_simple.py: -------------------------------------------------------------------------------- 1 | """Python file to serve as the frontend""" 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.path.abspath('.')) 6 | 7 | import streamlit as st 8 | import time 9 | from demo_app.components.sidebar import sidebar 10 | from langchain.chains import ConversationChain 11 | from langchain_openai import ChatOpenAI 12 | 13 | from langchain_core.output_parsers import StrOutputParser 14 | from langchain_core.prompts import ChatPromptTemplate 15 | 16 | 17 | def load_chain(): 18 | """Logic for loading the chain you want to use should go here.""" 19 | # llm = OpenAI(openai_api_key=st.session_state.get("OPENAI_API_KEY"), temperature=0) 20 | llm = ChatOpenAI( 21 | model="gpt-4o", 22 | openai_api_key=st.session_state.get("OPENAI_API_KEY"), 23 | temperature=0, 24 | max_tokens=None, 25 | timeout=None, 26 | max_retries=2, 27 | ) 28 | chain = ConversationChain(llm=llm) 29 | return chain 30 | 31 | 32 | def get_text(): 33 | input_text = st.text_input("You: ", "Hello, how are you?", key="input") 34 | return input_text 35 | 36 | 37 | if __name__ == "__main__": 38 | 39 | st.set_page_config( 40 | page_title="Chat App: LangChain Demo", 41 | page_icon="📖", 42 | layout="wide", 43 | initial_sidebar_state="expanded", ) 44 | st.header("📖 Chat App: LangChain Demo") 45 | sidebar() 46 | 47 | if not st.session_state.get("open_api_key_configured"): 48 | st.error("Please configure your API Keys!") 49 | else: 50 | chain = load_chain() 51 | 52 | if "messages" not in st.session_state: 53 | st.session_state["messages"] = [ 54 | {"role": "assistant", "content": "How can I help you?"}] 55 | 56 | # Display chat messages from history on app rerun 57 | for message in st.session_state.messages: 58 | with st.chat_message(message["role"]): 59 | st.markdown(message["content"]) 60 | 61 | if user_input := st.chat_input("What is your question?", accept_file=st.session_state["file_selection"], file_type=["jpg", "jpeg", "png", "pdf"]): 62 | # Add user message to chat history 63 | 64 | query = user_input.text 65 | if user_input["files"]: 66 | uploaded_files = user_input["files"] 67 | for f in uploaded_files: 68 | # Do something with the file 69 | print(f"File uploaded: {f.name}") 70 | 71 | st.session_state.messages.append({"role": "user", "content": query}) 72 | # Display user message in chat message container 73 | with st.chat_message("user"): 74 | st.markdown(query) 75 | 76 | with st.chat_message("assistant"): 77 | message_placeholder = st.empty() 78 | full_response = "" 79 | 80 | with st.chat_message("assistant"): 81 | message_placeholder = st.empty() 82 | full_response = "" 83 | 84 | with st.spinner('CHAT-BOT is at Work ...'): 85 | assistant_response = output = chain.invoke(input=query) 86 | # Simulate stream of response with milliseconds delay 87 | for chunk in assistant_response.split(): 88 | full_response += chunk + " " 89 | time.sleep(0.05) 90 | # Add a blinking cursor to simulate typing 91 | message_placeholder.markdown(full_response + "▌") 92 | message_placeholder.markdown(full_response) 93 | st.session_state.messages.append({"role": "assistant", "content": full_response}) 94 | 95 | -------------------------------------------------------------------------------- /demo_app/components/sidebar.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sidebar Component for Streamlit Chat Application 3 | Handles API key configuration and feature toggles 4 | """ 5 | import streamlit as st 6 | 7 | 8 | def sidebar(): 9 | """ 10 | Render sidebar with configuration options 11 | """ 12 | st.sidebar.title("⚙️ Configuration") 13 | 14 | # API Key Configuration 15 | st.sidebar.markdown("### 🔑 API Key") 16 | api_key = st.sidebar.text_input( 17 | "OpenAI API Key", 18 | type="password", 19 | key="OPENAI_API_KEY_INPUT", 20 | help="Enter your OpenAI API key. Get one at https://platform.openai.com/api-keys" 21 | ) 22 | 23 | if api_key: 24 | st.session_state["OPENAI_API_KEY"] = api_key 25 | st.session_state["open_api_key_configured"] = True 26 | st.sidebar.success("✅ API Key configured!") 27 | else: 28 | st.session_state["open_api_key_configured"] = False 29 | st.sidebar.warning("⚠️ Please enter your API key") 30 | 31 | st.sidebar.markdown("---") 32 | 33 | # Feature Toggles 34 | st.sidebar.markdown("### 🎛️ Features") 35 | 36 | # File upload toggle 37 | st.session_state["file_selection"] = st.sidebar.checkbox( 38 | "📄 Enable PDF Uploads", 39 | value=True, 40 | help="Allow uploading PDF documents for analysis" 41 | ) 42 | 43 | # Voice input toggle 44 | st.session_state["voice_selection"] = st.sidebar.checkbox( 45 | "🎤 Enable Voice Input", 46 | value=True, 47 | help="Enable voice-to-text using OpenAI Whisper" 48 | ) 49 | 50 | # TTS voice selection 51 | if st.session_state.get("voice_selection", True): 52 | st.session_state["tts_voice"] = st.sidebar.selectbox( 53 | "🔊 TTS Voice", 54 | options=["alloy", "echo", "fable", "onyx", "nova", "shimmer"], 55 | index=4, # Default to "nova" 56 | help="Select voice for text-to-speech responses" 57 | ) 58 | 59 | st.sidebar.markdown("---") 60 | 61 | # Session Management 62 | st.sidebar.markdown("### 💾 Session") 63 | 64 | # Session ID display 65 | session_id = st.session_state.get("session_id", "default_session") 66 | st.sidebar.text_input( 67 | "Session ID", 68 | value=session_id, 69 | disabled=True, 70 | help="Current conversation session identifier" 71 | ) 72 | 73 | # Clear conversation button 74 | if st.sidebar.button("🗑️ Clear Conversation", use_container_width=True): 75 | st.session_state.messages = [ 76 | { 77 | "role": "assistant", 78 | "content": "👋 Conversation cleared! How can I help you?" 79 | } 80 | ] 81 | st.session_state.pdf_content = "" 82 | st.rerun() 83 | 84 | st.sidebar.markdown("---") 85 | 86 | # Model Information 87 | st.sidebar.markdown("### 🤖 Model Info") 88 | st.sidebar.info( 89 | """ 90 | **Language Model:** GPT-4o 91 | **Speech-to-Text:** Whisper-1 92 | **Text-to-Speech:** TTS-1 93 | **Max PDF Size:** 25 MB 94 | """ 95 | ) 96 | 97 | st.sidebar.markdown("---") 98 | 99 | # Tips 100 | with st.sidebar.expander("💡 Tips & Tricks"): 101 | st.markdown(""" 102 | **Voice Input:** 103 | - Speak clearly and minimize background noise 104 | - Works in 99+ languages 105 | - Click the mic icon to record 106 | - Get audio responses automatically! 107 | 108 | **PDF Analysis:** 109 | - Upload one or multiple PDFs 110 | - Ask specific questions about the content 111 | - Context is maintained throughout the conversation 112 | 113 | **Audio Responses:** 114 | - When you use voice input, responses include audio 115 | - Choose your preferred voice in settings 116 | - Both text and audio are provided 117 | 118 | **Best Practices:** 119 | - Be specific in your questions 120 | - Reference previous messages in follow-ups 121 | - Use voice for quick queries 122 | """) 123 | 124 | # Footer 125 | st.sidebar.markdown("---") 126 | st.sidebar.markdown( 127 | """ 128 |
129 | Built with Streamlit & LangChain
130 | Powered by OpenAI 131 |
132 | """, 133 | unsafe_allow_html=True 134 | ) 135 | 136 | 137 | if __name__ == "__main__": 138 | # For testing the sidebar independently 139 | st.set_page_config(page_title="Sidebar Test", page_icon="⚙️") 140 | sidebar() 141 | st.title("Sidebar Component Test") 142 | st.write("Check the sidebar configuration →") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 📖 LangChain-Streamlit-Docker App Template 3 |

4 | 5 | ![UI](ui.PNG?raw=true) 6 | 7 | ## 🔧 Features 8 | 9 | - Basic Skeleton App configured with `openai` API 10 | - A ChatBot using LangChain and Streamlit 11 | - Upgraded to Langchain 1.x Ecosystem 12 | - users can upload one or multiple files in chat 13 | - User can ask Question using Text or Voice 14 | - Docker Support with Optimisation Cache etc 15 | - Deployment on Streamlit Public Cloud 16 | - Deployment on Google Cloud App Engine 17 | - Deployment on Google Cloud using `Cloud Run` 18 | 19 | This repo contains an `main.py` file which has a template for a chatbot implementation. 20 | 21 | ## Adding your chain 22 | To add your chain, you need to change the `load_chain` function in `main.py`. 23 | Depending on the type of your chain, you may also need to change the inputs/outputs that occur later on. 24 | 25 | 26 | ## 💻 Running Locally 27 | 28 | 1. Clone the repository📂 29 | 30 | ```bash 31 | git clone https://github.com/amjadraza/langchain-streamlit-docker-template.git 32 | ``` 33 | 34 | 2. Install dependencies with [uv](https://docs.astral.sh/uv/) and activate virtual environment🔨 35 | 36 | ```bash 37 | uv venv 38 | source .venv/bin/activate 39 | uv pip install -r pyproject.toml 40 | ``` 41 | 42 | if you are using remote machines like WSL2, to optimise lcoal developments, follow 43 | 44 | ```bash 45 | uv venv ~/.cache/pyuv/langchain-streamlit-docker-template 46 | source /home/{username}/.cache/pyuv/langchain-streamlit-docker-template/bin/activate 47 | uv pip install -r pyproject.toml 48 | ``` 49 | > You can name the environment. 50 | 51 | 52 | 1. Run the Streamlit server🚀 53 | 54 | ```bash 55 | streamlit run demo_app/main.py 56 | ``` 57 | 58 | Run App using Docker 59 | -------------------- 60 | This project includes `Dockerfile` to run the app in Docker container. In order to optimise the Docker Image 61 | size and building time with cache techniques, I have follow tricks in below Article 62 | https://medium.com/@albertazzir/blazing-fast-python-docker-builds-with-poetry-a78a66f5aed0 63 | 64 | Build the docker container 65 | 66 | ``docker build . -t langchain-chat-app:latest `` 67 | 68 | To generate Image with `DOCKER_BUILDKIT`, follow below command 69 | 70 | ```DOCKER_BUILDKIT=1 docker build --target=runtime . -t langchain-chat-app:latest``` 71 | 72 | 1. Run the docker container directly 73 | 74 | ``docker run -d --name langchain-chat-app -p 8080:8080 langchain-chat-app `` 75 | 76 | 2. Run the docker container using docker-compose (Recommended) 77 | 78 | ``docker-compose up`` 79 | 80 | 81 | Deploy App on Streamlit Public Cloud 82 | ------------------------------------ 83 | This app can be deployed on Streamlit Public Cloud using GitHub. Below is the Link to 84 | Publicly deployed App 85 | 86 | https://langchain-docker-template-amjadraza.streamlit.app/ 87 | 88 | 89 | Deploy App on Google App Engine 90 | -------------------------------- 91 | This app can be deployed on Google App Engine following below steps. 92 | 93 | ## Prerequisites 94 | 95 | Follow below guide on basic Instructions. 96 | [How to deploy Streamlit apps to Google App Engine](https://dev.to/whitphx/how-to-deploy-streamlit-apps-to-google-app-engine-407o) 97 | 98 | We added below tow configurations files 99 | 100 | 1. `app.yaml`: A Configuration file for `gcloud` 101 | 2. `.gcloudignore` : Configure the file to ignore file / folders to be uploaded 102 | 103 | I have adopted `Dockerfile` to deploy the app on GCP APP Engine. 104 | 105 | 1. Initialise & Configure the App 106 | 107 | ``gcloud app create --project=[YOUR_PROJECT_ID]`` 108 | 109 | 2. Deploy the App using 110 | 111 | ``gcloud app deploy`` 112 | 113 | 3. Access the App using 114 | 115 | https://langchain-chat.ts.r.appspot.com/ 116 | 117 | 118 | Deploy App on Google Cloud using Cloud Run 119 | ------------------------------------------ 120 | 121 | This app can be deployed on Google Cloud using Cloud Run following below steps. 122 | 123 | ## Prerequisites 124 | 125 | Follow below guide on basic Instructions. 126 | [How to deploy Streamlit apps to Google App Engine](https://dev.to/whitphx/how-to-deploy-streamlit-apps-to-google-app-engine-407o) 127 | 128 | We added below tow configurations files 129 | 130 | 1. `cloudbuild.yaml`: A Configuration file for `gcloud` 131 | 2. `.gcloudignore` : Configure the file to ignore file / folders to be uploaded 132 | 133 | we are going to use `Dockerfile` to deploy the app using Google Cloud Run. 134 | 135 | 1. Initialise & Configure the Google Project using Command Prompt 136 | 137 | `gcloud app create --project=[YOUR_PROJECT_ID]` 138 | 139 | 2. Enable Services for the Project 140 | 141 | ``` 142 | gcloud services enable cloudbuild.googleapis.com 143 | gcloud services enable run.googleapis.com 144 | ``` 145 | 146 | 3. Create Service Account 147 | 148 | ``` 149 | gcloud iam service-accounts create langchain-app-cr \ 150 | --display-name="langchain-app-cr" 151 | 152 | gcloud projects add-iam-policy-binding langchain-chat \ 153 | --member="serviceAccount:langchain-app-cr@langchain-chat.iam.gserviceaccount.com" \ 154 | --role="roles/run.invoker" 155 | 156 | gcloud projects add-iam-policy-binding langchain-chat \ 157 | --member="serviceAccount:langchain-app-cr@langchain-chat.iam.gserviceaccount.com" \ 158 | --role="roles/serviceusage.serviceUsageConsumer" 159 | 160 | gcloud projects add-iam-policy-binding langchain-chat \ 161 | --member="serviceAccount:langchain-app-cr@langchain-chat.iam.gserviceaccount.com" \ 162 | --role="roles/run.admin" 163 | ``` 164 | 165 | 4. Generate the Docker 166 | 167 | `DOCKER_BUILDKIT=1 docker build --target=runtime . -t australia-southeast1-docker.pkg.dev/langchain-chat/app/langchain-chat-app:latest` 168 | 169 | 5. Push Image to Google Artifact's Registry 170 | 171 | `configure-docker` authentication 172 | 173 | `gcloud auth configure-docker australia-southeast1-docker.pkg.dev` 174 | 175 | In order to push the `docker-image` to Artifact registry, first create app in the region of choice. 176 | 177 | Check the artifacts locations 178 | 179 | `gcloud artifacts locations list` 180 | 181 | Create the repository with name `app` 182 | 183 | ``` 184 | gcloud artifacts repositories create app \ 185 | --repository-format=docker \ 186 | --location=australia-southeast1 \ 187 | --description="A Langachain Streamlit App" \ 188 | --async 189 | ``` 190 | 191 | Once ready, let us push the image to location 192 | 193 | `docker push australia-southeast1-docker.pkg.dev/langchain-chat/app/langchain-chat-app:latest` 194 | 195 | 6. Deploy using Cloud Run 196 | 197 | Once image is pushed to Google Cloud Artifacts Registry. Let us deploy the image. 198 | 199 | ``` 200 | gcloud run deploy langchain-chat-app --image=australia-southeast1-docker.pkg.dev/langchain-chat/app/langchain-chat-app:latest \ 201 | --region=australia-southeast1 \ 202 | --service-account=langchain-app-cr@langchain-chat.iam.gserviceaccount.com 203 | ``` 204 | 205 | ## Report Feedbacks 206 | 207 | As `langchain-streamlit-docker-template` is a template project with minimal example. Report issues if you face any. 208 | 209 | ## DISCLAIMER 210 | 211 | This is a template App, when using with openai_api key, you will be charged a nominal fee depending 212 | on number of prompts etc. -------------------------------------------------------------------------------- /demo_app/main_lg0.py: -------------------------------------------------------------------------------- 1 | """Python file to serve as the frontend""" 2 | import sys 3 | import os 4 | sys.path.append(os.path.abspath('.')) 5 | 6 | import streamlit as st 7 | import time 8 | import tempfile 9 | from demo_app.components.sidebar import sidebar 10 | from langchain.chains import ConversationChain 11 | from langchain_openai import ChatOpenAI 12 | 13 | from langchain_core.output_parsers import StrOutputParser 14 | from langchain_core.prompts import ChatPromptTemplate 15 | from langchain_community.document_loaders import PyPDFLoader 16 | 17 | def load_chain(): 18 | """Logic for loading the chain you want to use should go here.""" 19 | llm = ChatOpenAI( 20 | model="gpt-4o", 21 | openai_api_key=st.session_state.get("OPENAI_API_KEY"), 22 | temperature=0, 23 | max_tokens=None, 24 | timeout=None, 25 | max_retries=2, 26 | ) 27 | chain = ConversationChain(llm=llm) 28 | return chain 29 | 30 | def process_pdf(uploaded_file): 31 | """Process the PDF file and return its content.""" 32 | # Create a temporary file 33 | with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file: 34 | # Write the uploaded file content to the temporary file 35 | tmp_file.write(uploaded_file.getvalue()) 36 | tmp_path = tmp_file.name 37 | 38 | # Use PyPDFLoader to load the PDF 39 | loader = PyPDFLoader(tmp_path) 40 | pages = loader.load() 41 | 42 | # Extract text from all pages 43 | pdf_text = "" 44 | for page in pages: 45 | pdf_text += page.page_content + "\n\n" 46 | 47 | # Clean up the temporary file 48 | os.unlink(tmp_path) 49 | 50 | return pdf_text 51 | 52 | def get_response_with_pdf_context(chain, query, pdf_content): 53 | """Get response from LLM with PDF content as context.""" 54 | prompt = ChatPromptTemplate.from_template( 55 | """You are a helpful assistant that answers questions based on the provided context. 56 | 57 | Context from PDF: 58 | {pdf_content} 59 | 60 | Human: {query} 61 | 62 | Assistant:""" 63 | ) 64 | 65 | # Prepare the prompt with PDF content and query 66 | formatted_prompt = prompt.format(pdf_content=pdf_content, query=query) 67 | 68 | # Get response from the LLM 69 | response = chain.llm.predict(formatted_prompt) 70 | 71 | return response 72 | 73 | if __name__ == "__main__": 74 | st.set_page_config( 75 | page_title="Chat App: LangChain Demo", 76 | page_icon="📖", 77 | layout="wide", 78 | initial_sidebar_state="expanded", 79 | ) 80 | st.header("📖 Chat App: LangChain Demo") 81 | sidebar() 82 | 83 | if not st.session_state.get("open_api_key_configured"): 84 | st.error("Please configure your API Keys!") 85 | else: 86 | chain = load_chain() 87 | 88 | # Initialize PDF content in session state 89 | if "pdf_content" not in st.session_state: 90 | st.session_state["pdf_content"] = "" 91 | 92 | if "messages" not in st.session_state: 93 | st.session_state["messages"] = [ 94 | {"role": "assistant", "content": "How can I help you? You can upload a PDF document, and I'll use its content to provide better answers."} 95 | ] 96 | 97 | # Display chat messages from history on app rerun 98 | for message in st.session_state.messages: 99 | with st.chat_message(message["role"]): 100 | st.markdown(message["content"]) 101 | 102 | # Handle user input 103 | user_input = st.chat_input("What is your question?", accept_file=st.session_state["file_selection"], file_type=["pdf"]) 104 | 105 | if user_input: 106 | pdf_uploaded = False 107 | query = user_input.text if hasattr(user_input, 'text') else user_input 108 | 109 | # Check if files were uploaded 110 | if hasattr(user_input, 'files') and user_input.files: 111 | uploaded_files = user_input.files 112 | 113 | # Process each uploaded PDF file 114 | all_pdf_content = "" 115 | processed_files = [] 116 | 117 | for uploaded_file in uploaded_files: 118 | if uploaded_file.name.lower().endswith('.pdf'): 119 | processed_files.append(uploaded_file.name) 120 | with st.spinner(f'Processing PDF: {uploaded_file.name}...'): 121 | pdf_content = process_pdf(uploaded_file) 122 | all_pdf_content += f"\n\n=== Content from: {uploaded_file.name} ===\n\n{pdf_content}" 123 | pdf_uploaded = True 124 | 125 | if processed_files: 126 | file_names = ", ".join(processed_files) 127 | st.session_state.messages.append({"role": "user", "content": f"Uploaded PDFs: {file_names}"}) 128 | with st.chat_message("user"): 129 | st.markdown(f"Uploaded PDFs: {file_names}") 130 | 131 | # Store the concatenated content from all PDFs 132 | st.session_state["pdf_content"] = all_pdf_content 133 | 134 | with st.chat_message("assistant"): 135 | st.markdown(f"PDF processed successfully. You can now ask questions about the content of {uploaded_file.name}.") 136 | st.session_state.messages.append({"role": "assistant", "content": f"PDF processed successfully. You can now ask questions about the content of {uploaded_file.name}."}) 137 | 138 | # If there is a text query, process it 139 | if query: 140 | st.session_state.messages.append({"role": "user", "content": query}) 141 | with st.chat_message("user"): 142 | st.markdown(query) 143 | 144 | with st.chat_message("assistant"): 145 | message_placeholder = st.empty() 146 | full_response = "" 147 | 148 | with st.spinner('CHAT-BOT is at Work ...'): 149 | # If we have PDF content, use it as context 150 | # if st.session_state["pdf_content"]: 151 | assistant_response = get_response_with_pdf_context(chain, query, st.session_state["pdf_content"]) 152 | # else: 153 | # # Otherwise, use the normal chain 154 | # assistant_response = chain.invoke(input=query) 155 | 156 | # Simulate stream of response with milliseconds delay 157 | for chunk in assistant_response.split(): 158 | full_response += chunk + " " 159 | time.sleep(0.05) 160 | # Add a blinking cursor to simulate typing 161 | message_placeholder.markdown(full_response + "▌") 162 | message_placeholder.markdown(full_response) 163 | 164 | st.session_state.messages.append({"role": "assistant", "content": full_response}) -------------------------------------------------------------------------------- /demo_app/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Upgraded Streamlit Chat Application with Voice Support 3 | - Modern LangChain patterns (RunnableWithMessageHistory) 4 | - Voice input with OpenAI Whisper STT 5 | - PDF processing with RAG 6 | - Latest Streamlit audio features 7 | """ 8 | import sys 9 | import os 10 | sys.path.append(os.path.abspath('.')) 11 | 12 | import streamlit as st 13 | import time 14 | import tempfile 15 | from typing import Optional 16 | 17 | # Modern LangChain imports 18 | from langchain_openai import ChatOpenAI 19 | from langchain_core.chat_history import InMemoryChatMessageHistory 20 | from langchain_core.runnables.history import RunnableWithMessageHistory 21 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 22 | from langchain_community.document_loaders import PyPDFLoader 23 | 24 | # OpenAI for Whisper STT 25 | from openai import OpenAI 26 | 27 | # Import sidebar component (assuming it exists) 28 | try: 29 | from demo_app.components.sidebar import sidebar 30 | except ImportError: 31 | def sidebar(): 32 | """Fallback sidebar if import fails""" 33 | st.sidebar.title("Settings") 34 | api_key = st.sidebar.text_input( 35 | "OpenAI API Key", 36 | type="password", 37 | key="OPENAI_API_KEY", 38 | help="Enter your OpenAI API key" 39 | ) 40 | if api_key: 41 | st.session_state["OPENAI_API_KEY"] = api_key 42 | st.session_state["open_api_key_configured"] = True 43 | 44 | # File upload selection 45 | st.session_state["file_selection"] = st.sidebar.checkbox( 46 | "Enable file uploads", 47 | value=True 48 | ) 49 | 50 | # Voice input selection 51 | st.session_state["voice_selection"] = st.sidebar.checkbox( 52 | "Enable voice input", 53 | value=True 54 | ) 55 | 56 | 57 | # Store for conversation history 58 | store = {} 59 | 60 | 61 | def get_session_history(session_id: str) -> InMemoryChatMessageHistory: 62 | """Get or create conversation history for a session""" 63 | if session_id not in store: 64 | store[session_id] = InMemoryChatMessageHistory() 65 | return store[session_id] 66 | 67 | 68 | def create_conversational_chain(api_key: str, pdf_content: str = ""): 69 | """ 70 | Create a modern conversational chain with message history support 71 | 72 | Args: 73 | api_key: OpenAI API key 74 | pdf_content: Optional PDF content to use as context 75 | 76 | Returns: 77 | RunnableWithMessageHistory chain 78 | """ 79 | # Initialize the LLM 80 | llm = ChatOpenAI( 81 | model="gpt-4o", 82 | api_key=api_key, 83 | temperature=0, 84 | streaming=True 85 | ) 86 | 87 | # Create prompt template with optional PDF context 88 | if pdf_content: 89 | prompt = ChatPromptTemplate.from_messages([ 90 | ("system", """You are a helpful assistant that answers questions based on the conversation history 91 | and the provided PDF context. 92 | 93 | PDF Context: 94 | {pdf_content} 95 | 96 | Use this context to provide accurate and relevant answers."""), 97 | MessagesPlaceholder(variable_name="history"), 98 | ("human", "{input}") 99 | ]) 100 | # Create chain with prompt and LLM 101 | chain = prompt | llm 102 | else: 103 | prompt = ChatPromptTemplate.from_messages([ 104 | ("system", "You are a helpful assistant. Answer questions based on the conversation history."), 105 | MessagesPlaceholder(variable_name="history"), 106 | ("human", "{input}") 107 | ]) 108 | # Create chain with prompt and LLM 109 | chain = prompt | llm 110 | 111 | # Wrap with message history 112 | conversational_chain = RunnableWithMessageHistory( 113 | chain, 114 | get_session_history, 115 | input_messages_key="input", 116 | history_messages_key="history", 117 | ) 118 | 119 | return conversational_chain, pdf_content if pdf_content else None 120 | 121 | 122 | def transcribe_audio(audio_bytes, api_key: str) -> str: 123 | """ 124 | Transcribe audio using OpenAI Whisper API 125 | 126 | Args: 127 | audio_bytes: Audio file bytes 128 | api_key: OpenAI API key 129 | 130 | Returns: 131 | Transcribed text 132 | """ 133 | try: 134 | client = OpenAI(api_key=api_key) 135 | 136 | # Create a temporary file to save the audio 137 | with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp_file: 138 | tmp_file.write(audio_bytes.getvalue()) 139 | tmp_path = tmp_file.name 140 | 141 | # Transcribe using Whisper 142 | with open(tmp_path, "rb") as audio_file: 143 | transcript = client.audio.transcriptions.create( 144 | model="whisper-1", 145 | file=audio_file, 146 | response_format="text" 147 | ) 148 | 149 | # Clean up temporary file 150 | os.unlink(tmp_path) 151 | 152 | return transcript 153 | 154 | except Exception as e: 155 | st.error(f"Error transcribing audio: {str(e)}") 156 | return "" 157 | 158 | 159 | def text_to_speech(text: str, api_key: str, voice: str = "nova") -> bytes: 160 | """ 161 | Convert text to speech using OpenAI TTS API 162 | 163 | Args: 164 | text: Text to convert to speech 165 | api_key: OpenAI API key 166 | voice: Voice to use (alloy, echo, fable, onyx, nova, shimmer) 167 | 168 | Returns: 169 | Audio bytes 170 | """ 171 | try: 172 | client = OpenAI(api_key=api_key) 173 | 174 | # Generate speech using TTS 175 | response = client.audio.speech.create( 176 | model="tts-1", 177 | voice=voice, 178 | input=text 179 | ) 180 | 181 | # Return audio bytes 182 | return response.content 183 | 184 | except Exception as e: 185 | st.error(f"Error generating speech: {str(e)}") 186 | return None 187 | 188 | 189 | def process_pdf(uploaded_file) -> str: 190 | """ 191 | Process the PDF file and return its content 192 | 193 | Args: 194 | uploaded_file: Streamlit uploaded file object 195 | 196 | Returns: 197 | Extracted text from PDF 198 | """ 199 | # Create a temporary file 200 | with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file: 201 | tmp_file.write(uploaded_file.getvalue()) 202 | tmp_path = tmp_file.name 203 | 204 | try: 205 | # Use PyPDFLoader to load the PDF 206 | loader = PyPDFLoader(tmp_path) 207 | pages = loader.load() 208 | 209 | # Extract text from all pages 210 | pdf_text = "\n\n".join([page.page_content for page in pages]) 211 | 212 | return pdf_text 213 | 214 | finally: 215 | # Clean up the temporary file 216 | os.unlink(tmp_path) 217 | 218 | 219 | def display_message_with_animation(message: str, placeholder) -> str: 220 | """ 221 | Display message with typing animation 222 | 223 | Args: 224 | message: Message to display 225 | placeholder: Streamlit placeholder for the message 226 | 227 | Returns: 228 | Full message 229 | """ 230 | full_response = "" 231 | for chunk in message.split(): 232 | full_response += chunk + " " 233 | time.sleep(0.05) 234 | placeholder.markdown(full_response + "▌") 235 | 236 | placeholder.markdown(full_response) 237 | return full_response 238 | 239 | 240 | def main(): 241 | """Main application function""" 242 | 243 | # Page configuration 244 | st.set_page_config( 245 | page_title="AI Chat with Voice & Audio Responses", 246 | page_icon="🤖", 247 | layout="wide", 248 | initial_sidebar_state="expanded", 249 | ) 250 | 251 | st.header("🤖 AI Chat Assistant") 252 | st.markdown("*With Voice Input, Audio Responses & PDF Analysis*") 253 | 254 | # Initialize sidebar 255 | sidebar() 256 | 257 | # Check if API key is configured 258 | if not st.session_state.get("open_api_key_configured"): 259 | st.error("⚠️ Please configure your OpenAI API Key in the sidebar!") 260 | st.stop() 261 | 262 | # Initialize session state variables 263 | if "messages" not in st.session_state: 264 | st.session_state.messages = [ 265 | { 266 | "role": "assistant", 267 | "content": "👋 Hello! I'm your AI assistant. You can:\n- Type your questions\n- 🎤 Use voice input (get audio responses!)\n- 📄 Upload PDF documents for analysis" 268 | } 269 | ] 270 | 271 | if "pdf_content" not in st.session_state: 272 | st.session_state.pdf_content = "" 273 | 274 | if "session_id" not in st.session_state: 275 | st.session_state.session_id = "default_session" 276 | 277 | if "file_selection" not in st.session_state: 278 | st.session_state.file_selection = True 279 | 280 | if "voice_selection" not in st.session_state: 281 | st.session_state.voice_selection = True 282 | 283 | if "audio_input_used" not in st.session_state: 284 | st.session_state.audio_input_used = False 285 | 286 | if "tts_voice" not in st.session_state: 287 | st.session_state.tts_voice = "nova" 288 | 289 | # Display chat history 290 | for message in st.session_state.messages: 291 | with st.chat_message(message["role"]): 292 | st.markdown(message["content"]) 293 | 294 | # Chat input with voice and file support 295 | prompt = st.chat_input( 296 | "Type your message or use voice input...", 297 | accept_file=st.session_state.get("file_selection", True), 298 | file_type=["pdf"], 299 | accept_audio=st.session_state.get("voice_selection", True) 300 | ) 301 | 302 | # Process user input 303 | if prompt: 304 | user_text = None 305 | audio_transcription = None 306 | 307 | # Check if audio was provided 308 | if hasattr(prompt, 'audio') and prompt.audio: 309 | st.session_state.audio_input_used = True # Track audio usage 310 | 311 | with st.spinner("🎤 Transcribing audio..."): 312 | audio_transcription = transcribe_audio( 313 | prompt.audio, 314 | st.session_state.get("OPENAI_API_KEY") 315 | ) 316 | 317 | if audio_transcription: 318 | # Display transcription 319 | st.session_state.messages.append({ 320 | "role": "user", 321 | "content": f"🎤 Voice: {audio_transcription}" 322 | }) 323 | with st.chat_message("user"): 324 | st.markdown(f"🎤 Voice: {audio_transcription}") 325 | 326 | user_text = audio_transcription 327 | else: 328 | st.session_state.audio_input_used = False # Reset flag for text input 329 | 330 | # Check if text was provided 331 | if hasattr(prompt, 'text') and prompt.text: 332 | user_text = prompt.text 333 | st.session_state.messages.append({ 334 | "role": "user", 335 | "content": user_text 336 | }) 337 | with st.chat_message("user"): 338 | st.markdown(user_text) 339 | 340 | # Check if files were uploaded 341 | if hasattr(prompt, 'files') and prompt.files: 342 | processed_files = [] 343 | all_pdf_content = "" 344 | 345 | for uploaded_file in prompt.files: 346 | if uploaded_file.name.lower().endswith('.pdf'): 347 | with st.spinner(f'📄 Processing {uploaded_file.name}...'): 348 | pdf_content = process_pdf(uploaded_file) 349 | all_pdf_content += f"\n\n=== Content from: {uploaded_file.name} ===\n\n{pdf_content}" 350 | processed_files.append(uploaded_file.name) 351 | 352 | if processed_files: 353 | file_names = ", ".join(processed_files) 354 | message = f"📄 Uploaded: {file_names}" 355 | 356 | st.session_state.messages.append({ 357 | "role": "user", 358 | "content": message 359 | }) 360 | with st.chat_message("user"): 361 | st.markdown(message) 362 | 363 | # Update PDF content in session state 364 | st.session_state.pdf_content = all_pdf_content 365 | 366 | # Acknowledge PDF processing 367 | ack_message = f"✅ PDF processed! I've analyzed {len(processed_files)} document(s). You can now ask questions about the content." 368 | st.session_state.messages.append({ 369 | "role": "assistant", 370 | "content": ack_message 371 | }) 372 | with st.chat_message("assistant"): 373 | st.markdown(ack_message) 374 | 375 | # Generate response if there's text to process 376 | if user_text: 377 | with st.chat_message("assistant"): 378 | message_placeholder = st.empty() 379 | 380 | with st.spinner("🤔 Thinking..."): 381 | # Create the chain with current PDF content 382 | chain, pdf_ctx = create_conversational_chain( 383 | api_key=st.session_state.get("OPENAI_API_KEY"), 384 | pdf_content=st.session_state.get("pdf_content", "") 385 | ) 386 | 387 | # Prepare input with PDF content if available 388 | chain_input = {"input": user_text} 389 | if pdf_ctx: 390 | chain_input["pdf_content"] = pdf_ctx 391 | 392 | # Get response with conversation history 393 | response = chain.invoke( 394 | chain_input, 395 | config={"configurable": {"session_id": st.session_state.session_id}} 396 | ) 397 | 398 | # Extract text from AIMessage 399 | response_text = response.content if hasattr(response, 'content') else str(response) 400 | 401 | # Display response with animation 402 | full_response = display_message_with_animation(response_text, message_placeholder) 403 | 404 | # Generate TTS audio if audio input was used 405 | if st.session_state.audio_input_used: 406 | with st.spinner("🔊 Generating audio response..."): 407 | selected_voice = st.session_state.get("tts_voice", "nova") 408 | audio_response = text_to_speech( 409 | full_response, 410 | st.session_state.get("OPENAI_API_KEY"), 411 | voice=selected_voice 412 | ) 413 | 414 | if audio_response: 415 | # Display audio player 416 | st.audio(audio_response, format="audio/mp3") 417 | 418 | # Add to message history 419 | st.session_state.messages.append({ 420 | "role": "assistant", 421 | "content": full_response 422 | }) 423 | 424 | 425 | if __name__ == "__main__": 426 | main() --------------------------------------------------------------------------------