Spaces:
Configuration error
Configuration error
MD MAFJUJUL KARIM
commited on
Commit
·
39fa096
1
Parent(s):
f646ccc
Initial commit for Docker LangGraph app
Browse files- .env +2 -0
- Dockerfile +10 -13
- Procfile +1 -0
- README.md +150 -20
- agent.py +32 -0
- app.py +551 -0
- docker-compose.yml +12 -0
- requirements.txt +8 -3
- test.ipynb +90 -0
.env
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OPENAI_API_KEY="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"
|
| 2 |
+
TAVILY_API_KEY="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"
|
Dockerfile
CHANGED
|
@@ -1,21 +1,18 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
curl \
|
| 8 |
-
software-properties-common \
|
| 9 |
-
git \
|
| 10 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
|
|
|
| 16 |
|
|
|
|
| 17 |
EXPOSE 8501
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# Copy requirements first for better caching
|
| 6 |
+
COPY requirements.txt .
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
# Install dependencies
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
|
| 11 |
+
# Copy the rest of the application
|
| 12 |
+
COPY . .
|
| 13 |
|
| 14 |
+
# Expose the port Streamlit will run on
|
| 15 |
EXPOSE 8501
|
| 16 |
|
| 17 |
+
# Command to run the application
|
| 18 |
+
CMD ["streamlit", "run", "app.py", "--server.address=0.0.0.0"]
|
|
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: streamlit run app.py --server.port=$PORT
|
README.md
CHANGED
|
@@ -1,20 +1,150 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Search Assistant
|
| 2 |
+
|
| 3 |
+
A web-based AI assistant that can search the internet to answer questions using LangChain, OpenAI, and Tavily Search.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- Interactive web interface built with Streamlit
|
| 8 |
+
- Powered by OpenAI's language models
|
| 9 |
+
- Real-time web search capabilities using Tavily Search
|
| 10 |
+
- Conversation history tracking
|
| 11 |
+
- Customizable search parameters
|
| 12 |
+
- Deployment-ready with in-app API key management
|
| 13 |
+
|
| 14 |
+
## How to run locally
|
| 15 |
+
|
| 16 |
+
1. Create and activate a conda environment:
|
| 17 |
+
```
|
| 18 |
+
conda create -n llmapp python=3.11 -y
|
| 19 |
+
conda activate llmapp
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
2. Install the required packages:
|
| 23 |
+
```
|
| 24 |
+
pip install -r requirements.txt
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
3. (Optional) Set up your API keys in a `.env` file:
|
| 28 |
+
```
|
| 29 |
+
OPENAI_API_KEY=your_openai_api_key
|
| 30 |
+
TAVILY_API_KEY=your_tavily_api_key
|
| 31 |
+
```
|
| 32 |
+
Note: You can also enter API keys directly in the app interface.
|
| 33 |
+
|
| 34 |
+
4. Run the Streamlit app:
|
| 35 |
+
```
|
| 36 |
+
streamlit run app.py
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
5. Open your browser and navigate to the URL shown in the terminal (typically http://localhost:8501)
|
| 40 |
+
|
| 41 |
+
## Deployment Options
|
| 42 |
+
|
| 43 |
+
### Deploy to Streamlit Cloud
|
| 44 |
+
|
| 45 |
+
1. Fork this repository to your GitHub account
|
| 46 |
+
2. Sign up for [Streamlit Cloud](https://streamlit.io/cloud)
|
| 47 |
+
3. Create a new app and connect it to your GitHub repository
|
| 48 |
+
4. Deploy the app (no environment variables needed as users will input their API keys)
|
| 49 |
+
|
| 50 |
+
### Deploy to Heroku
|
| 51 |
+
|
| 52 |
+
1. Create a Heroku account and install the Heroku CLI
|
| 53 |
+
2. Create a new Heroku app:
|
| 54 |
+
```
|
| 55 |
+
heroku create your-app-name
|
| 56 |
+
```
|
| 57 |
+
3. Add a `Procfile` with the following content:
|
| 58 |
+
```
|
| 59 |
+
web: streamlit run app.py --server.port=$PORT
|
| 60 |
+
```
|
| 61 |
+
4. Deploy to Heroku:
|
| 62 |
+
```
|
| 63 |
+
git push heroku main
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Deploy to Hugging Face using Docker
|
| 67 |
+
|
| 68 |
+
1. Create a Hugging Face account at [huggingface.co](https://huggingface.co/)
|
| 69 |
+
2. Install the Hugging Face CLI:
|
| 70 |
+
```
|
| 71 |
+
pip install huggingface_hub
|
| 72 |
+
```
|
| 73 |
+
3. Login to Hugging Face:
|
| 74 |
+
```
|
| 75 |
+
huggingface-cli login
|
| 76 |
+
```
|
| 77 |
+
4. Create a new Space on Hugging Face:
|
| 78 |
+
```
|
| 79 |
+
huggingface-cli repo create ai-search-assistant --type space
|
| 80 |
+
```
|
| 81 |
+
5. Clone your repository:
|
| 82 |
+
```
|
| 83 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/ai-search-assistant
|
| 84 |
+
```
|
| 85 |
+
6. Copy your project files to the cloned repository
|
| 86 |
+
7. Add a `.gitattributes` file with the following content:
|
| 87 |
+
```
|
| 88 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 89 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 90 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 91 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 92 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 93 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 94 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 95 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 96 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 97 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 98 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 99 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 100 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 101 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 102 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 103 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 104 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 105 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 106 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 107 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 108 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 109 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 110 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 111 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 112 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 113 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 114 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 115 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 116 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 117 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 118 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 119 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 120 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 121 |
+
```
|
| 122 |
+
8. Create a `Dockerfile` in your repository (already provided in this project)
|
| 123 |
+
9. Add a `README.md` file with a description of your app
|
| 124 |
+
10. Commit and push your changes:
|
| 125 |
+
```
|
| 126 |
+
git add .
|
| 127 |
+
git commit -m "Initial commit"
|
| 128 |
+
git push
|
| 129 |
+
```
|
| 130 |
+
11. Configure your Space on the Hugging Face website:
|
| 131 |
+
- Go to your Space settings
|
| 132 |
+
- Set the Space SDK to "Docker"
|
| 133 |
+
- Set the Hardware to your preferred option (CPU is sufficient for this app)
|
| 134 |
+
- Save your changes
|
| 135 |
+
12. Your app will be built and deployed automatically. You can access it at `https://huggingface.co/spaces/YOUR_USERNAME/ai-search-assistant`
|
| 136 |
+
|
| 137 |
+
## Usage
|
| 138 |
+
|
| 139 |
+
1. Enter your OpenAI and Tavily API keys in the sidebar
|
| 140 |
+
2. Configure search settings (model and number of results)
|
| 141 |
+
3. Enter your question in the text input field
|
| 142 |
+
4. Click the "Search" button
|
| 143 |
+
5. View the AI's response with information sourced from the web
|
| 144 |
+
6. Continue the conversation with follow-up questions
|
| 145 |
+
|
| 146 |
+
## Requirements
|
| 147 |
+
|
| 148 |
+
- Python 3.11+
|
| 149 |
+
- OpenAI API key ([Get one here](https://platform.openai.com/api-keys))
|
| 150 |
+
- Tavily API key ([Get one here](https://tavily.com/#api))
|
agent.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv, find_dotenv
|
| 3 |
+
_ = load_dotenv(find_dotenv())
|
| 4 |
+
from langchain_openai import ChatOpenAI
|
| 5 |
+
from langchain_community.tools.tavily_search import TavilySearchResults
|
| 6 |
+
from langgraph.prebuilt import create_react_agent
|
| 7 |
+
from langchain_core.messages import HumanMessage
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# Access the variables
|
| 12 |
+
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
|
| 13 |
+
TAVILY_API_KEY = os.getenv('TAVILY_API_KEY')
|
| 14 |
+
|
| 15 |
+
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
|
| 16 |
+
os.environ["TAVILY_API_KEY"] = TAVILY_API_KEY
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
chatModel = ChatOpenAI(model="gpt-3.5-turbo-0125")
|
| 20 |
+
search = TavilySearchResults(max_results=3)
|
| 21 |
+
|
| 22 |
+
# res = search.invoke("Tell me the recent movies list in 2025")
|
| 23 |
+
# print(res)
|
| 24 |
+
|
| 25 |
+
tool = [search]
|
| 26 |
+
|
| 27 |
+
agent_executor = create_react_agent(chatModel, tool)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
response = agent_executor.invoke({"messages": [HumanMessage(content="Tell me the recent movies list in 2025")]})
|
| 31 |
+
|
| 32 |
+
print(response['messages'])
|
app.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from dotenv import load_dotenv, find_dotenv
|
| 5 |
+
from langchain_openai import ChatOpenAI
|
| 6 |
+
from langchain_community.tools.tavily_search import TavilySearchResults
|
| 7 |
+
from langgraph.prebuilt import create_react_agent
|
| 8 |
+
from langchain_core.messages import HumanMessage
|
| 9 |
+
|
| 10 |
+
# Set page configuration - MUST BE THE FIRST STREAMLIT COMMAND
|
| 11 |
+
st.set_page_config(
|
| 12 |
+
page_title="AI Search Assistant",
|
| 13 |
+
page_icon="🔍",
|
| 14 |
+
layout="wide",
|
| 15 |
+
initial_sidebar_state="expanded"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Load environment variables (for local development)
|
| 19 |
+
_ = load_dotenv(find_dotenv())
|
| 20 |
+
|
| 21 |
+
# Initialize session state for API keys if not already present
|
| 22 |
+
if "openai_api_key" not in st.session_state:
|
| 23 |
+
st.session_state.openai_api_key = os.getenv('OPENAI_API_KEY', '')
|
| 24 |
+
if "tavily_api_key" not in st.session_state:
|
| 25 |
+
st.session_state.tavily_api_key = os.getenv('TAVILY_API_KEY', '')
|
| 26 |
+
if "api_keys_valid" not in st.session_state:
|
| 27 |
+
st.session_state.api_keys_valid = False
|
| 28 |
+
if "messages" not in st.session_state:
|
| 29 |
+
st.session_state.messages = []
|
| 30 |
+
if "thinking" not in st.session_state:
|
| 31 |
+
st.session_state.thinking = False
|
| 32 |
+
|
| 33 |
+
# Custom CSS for a more professional look
|
| 34 |
+
st.markdown("""
|
| 35 |
+
<style>
|
| 36 |
+
.main-header {
|
| 37 |
+
font-family: 'Helvetica Neue', sans-serif;
|
| 38 |
+
font-weight: 700;
|
| 39 |
+
color: #1E88E5;
|
| 40 |
+
}
|
| 41 |
+
.sub-header {
|
| 42 |
+
font-family: 'Helvetica Neue', sans-serif;
|
| 43 |
+
font-weight: 600;
|
| 44 |
+
color: #333;
|
| 45 |
+
}
|
| 46 |
+
.chat-message {
|
| 47 |
+
padding: 1.5rem;
|
| 48 |
+
border-radius: 0.8rem;
|
| 49 |
+
margin-bottom: 1rem;
|
| 50 |
+
display: flex;
|
| 51 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
| 52 |
+
}
|
| 53 |
+
.chat-message.user {
|
| 54 |
+
background-color: #E3F2FD;
|
| 55 |
+
border-left: 5px solid #1E88E5;
|
| 56 |
+
}
|
| 57 |
+
.chat-message.assistant {
|
| 58 |
+
background-color: #F5F5F5;
|
| 59 |
+
border-left: 5px solid #4CAF50;
|
| 60 |
+
}
|
| 61 |
+
.chat-message .avatar {
|
| 62 |
+
width: 40px;
|
| 63 |
+
height: 40px;
|
| 64 |
+
border-radius: 50%;
|
| 65 |
+
object-fit: cover;
|
| 66 |
+
margin-right: 1rem;
|
| 67 |
+
}
|
| 68 |
+
.chat-message .message {
|
| 69 |
+
flex-grow: 1;
|
| 70 |
+
}
|
| 71 |
+
.highlight {
|
| 72 |
+
background-color: #E3F2FD;
|
| 73 |
+
padding: 0.2rem 0.5rem;
|
| 74 |
+
border-radius: 0.3rem;
|
| 75 |
+
font-weight: bold;
|
| 76 |
+
color: #1E88E5;
|
| 77 |
+
}
|
| 78 |
+
.stButton button {
|
| 79 |
+
background-color: #1E88E5;
|
| 80 |
+
color: white;
|
| 81 |
+
border-radius: 20px;
|
| 82 |
+
padding: 0.5rem 1rem;
|
| 83 |
+
border: none;
|
| 84 |
+
font-weight: bold;
|
| 85 |
+
transition: all 0.3s;
|
| 86 |
+
}
|
| 87 |
+
.stButton button:hover {
|
| 88 |
+
background-color: #1565C0;
|
| 89 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
| 90 |
+
}
|
| 91 |
+
.clear-button button {
|
| 92 |
+
background-color: #F5F5F5;
|
| 93 |
+
color: #333;
|
| 94 |
+
border: 1px solid #ddd;
|
| 95 |
+
}
|
| 96 |
+
.clear-button button:hover {
|
| 97 |
+
background-color: #EEEEEE;
|
| 98 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
| 99 |
+
}
|
| 100 |
+
.api-form {
|
| 101 |
+
background-color: #F5F5F5;
|
| 102 |
+
padding: 1.5rem;
|
| 103 |
+
border-radius: 0.8rem;
|
| 104 |
+
margin-bottom: 1rem;
|
| 105 |
+
border: 1px solid #ddd;
|
| 106 |
+
}
|
| 107 |
+
.source-link {
|
| 108 |
+
font-size: 0.8rem;
|
| 109 |
+
color: #1E88E5;
|
| 110 |
+
text-decoration: none;
|
| 111 |
+
}
|
| 112 |
+
.source-link:hover {
|
| 113 |
+
text-decoration: underline;
|
| 114 |
+
}
|
| 115 |
+
.thinking-animation {
|
| 116 |
+
display: inline-block;
|
| 117 |
+
position: relative;
|
| 118 |
+
width: 80px;
|
| 119 |
+
height: 20px;
|
| 120 |
+
}
|
| 121 |
+
.thinking-animation div {
|
| 122 |
+
position: absolute;
|
| 123 |
+
top: 8px;
|
| 124 |
+
width: 10px;
|
| 125 |
+
height: 10px;
|
| 126 |
+
border-radius: 50%;
|
| 127 |
+
background: #1E88E5;
|
| 128 |
+
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
| 129 |
+
}
|
| 130 |
+
.thinking-animation div:nth-child(1) {
|
| 131 |
+
left: 8px;
|
| 132 |
+
animation: thinking1 0.6s infinite;
|
| 133 |
+
}
|
| 134 |
+
.thinking-animation div:nth-child(2) {
|
| 135 |
+
left: 8px;
|
| 136 |
+
animation: thinking2 0.6s infinite;
|
| 137 |
+
}
|
| 138 |
+
.thinking-animation div:nth-child(3) {
|
| 139 |
+
left: 32px;
|
| 140 |
+
animation: thinking2 0.6s infinite;
|
| 141 |
+
}
|
| 142 |
+
.thinking-animation div:nth-child(4) {
|
| 143 |
+
left: 56px;
|
| 144 |
+
animation: thinking3 0.6s infinite;
|
| 145 |
+
}
|
| 146 |
+
@keyframes thinking1 {
|
| 147 |
+
0% {transform: scale(0);}
|
| 148 |
+
100% {transform: scale(1);}
|
| 149 |
+
}
|
| 150 |
+
@keyframes thinking3 {
|
| 151 |
+
0% {transform: scale(1);}
|
| 152 |
+
100% {transform: scale(0);}
|
| 153 |
+
}
|
| 154 |
+
@keyframes thinking2 {
|
| 155 |
+
0% {transform: translate(0, 0);}
|
| 156 |
+
100% {transform: translate(24px, 0);}
|
| 157 |
+
}
|
| 158 |
+
.stTextInput input {
|
| 159 |
+
border-radius: 20px;
|
| 160 |
+
padding: 0.5rem 1rem;
|
| 161 |
+
border: 1px solid #ddd;
|
| 162 |
+
}
|
| 163 |
+
.stTextInput input:focus {
|
| 164 |
+
border-color: #1E88E5;
|
| 165 |
+
box-shadow: 0 0 0 0.2rem rgba(30, 136, 229, 0.25);
|
| 166 |
+
}
|
| 167 |
+
.sidebar-content {
|
| 168 |
+
background-color: #F5F5F5;
|
| 169 |
+
padding: 1rem;
|
| 170 |
+
border-radius: 0.8rem;
|
| 171 |
+
margin-bottom: 1rem;
|
| 172 |
+
}
|
| 173 |
+
.footer {
|
| 174 |
+
text-align: center;
|
| 175 |
+
margin-top: 2rem;
|
| 176 |
+
padding-top: 1rem;
|
| 177 |
+
border-top: 1px solid #ddd;
|
| 178 |
+
color: #666;
|
| 179 |
+
font-size: 0.8rem;
|
| 180 |
+
}
|
| 181 |
+
.stTabs [data-baseweb="tab-list"] {
|
| 182 |
+
gap: 2px;
|
| 183 |
+
}
|
| 184 |
+
.stTabs [data-baseweb="tab"] {
|
| 185 |
+
background-color: #F5F5F5;
|
| 186 |
+
border-radius: 4px 4px 0px 0px;
|
| 187 |
+
padding: 10px 20px;
|
| 188 |
+
border: none;
|
| 189 |
+
}
|
| 190 |
+
.stTabs [aria-selected="true"] {
|
| 191 |
+
background-color: #1E88E5 !important;
|
| 192 |
+
color: white !important;
|
| 193 |
+
}
|
| 194 |
+
</style>
|
| 195 |
+
""", unsafe_allow_html=True)
|
| 196 |
+
|
| 197 |
+
# App title and description
|
| 198 |
+
st.markdown('<h1 class="main-header">🔍 AI Search Assistant</h1>', unsafe_allow_html=True)
|
| 199 |
+
|
| 200 |
+
# Create tabs for different sections
|
| 201 |
+
tabs = st.tabs(["🤖 Chat", "ℹ️ About", "🛠️ Settings"])
|
| 202 |
+
|
| 203 |
+
with tabs[0]: # Chat Tab
|
| 204 |
+
if st.session_state.api_keys_valid:
|
| 205 |
+
# Chat interface
|
| 206 |
+
st.markdown('<h3 class="sub-header">Ask me anything</h3>', unsafe_allow_html=True)
|
| 207 |
+
|
| 208 |
+
# Query input with dynamic placeholder
|
| 209 |
+
placeholders = [
|
| 210 |
+
"e.g., What are the latest developments in AI?",
|
| 211 |
+
"e.g., Tell me about recent movies in 2025",
|
| 212 |
+
"e.g., What are the best tourist spots in Japan?",
|
| 213 |
+
"e.g., How does quantum computing work?",
|
| 214 |
+
"e.g., What are the trending technologies in 2025?"
|
| 215 |
+
]
|
| 216 |
+
import random
|
| 217 |
+
query = st.text_input("", placeholder=random.choice(placeholders), key="query_input")
|
| 218 |
+
|
| 219 |
+
# Buttons
|
| 220 |
+
col1, col2 = st.columns([1, 5])
|
| 221 |
+
with col1:
|
| 222 |
+
search_button = st.button("🔍 Search", use_container_width=True)
|
| 223 |
+
with col2:
|
| 224 |
+
clear_button = st.button("🗑️ Clear Chat", use_container_width=False, key="clear_button")
|
| 225 |
+
st.markdown('<div class="clear-button"></div>', unsafe_allow_html=True)
|
| 226 |
+
|
| 227 |
+
# Chat container
|
| 228 |
+
st.markdown('<h3 class="sub-header">Conversation</h3>', unsafe_allow_html=True)
|
| 229 |
+
chat_container = st.container(height=500)
|
| 230 |
+
|
| 231 |
+
# Process the query when submitted
|
| 232 |
+
if query and search_button:
|
| 233 |
+
# Add user message to chat history
|
| 234 |
+
st.session_state.messages.append({"role": "user", "content": query})
|
| 235 |
+
st.session_state.thinking = True
|
| 236 |
+
|
| 237 |
+
# Force a rerun to show the user message immediately
|
| 238 |
+
st.experimental_rerun()
|
| 239 |
+
|
| 240 |
+
# Display chat messages
|
| 241 |
+
with chat_container:
|
| 242 |
+
if not st.session_state.messages:
|
| 243 |
+
st.info("👋 Hello! Ask me anything and I'll search the web for answers.")
|
| 244 |
+
|
| 245 |
+
for message in st.session_state.messages:
|
| 246 |
+
if message["role"] == "user":
|
| 247 |
+
st.markdown(f"""
|
| 248 |
+
<div class="chat-message user">
|
| 249 |
+
<img src="https://api.dicebear.com/7.x/bottts/svg?seed=user" class="avatar" alt="user">
|
| 250 |
+
<div class="message">{message["content"]}</div>
|
| 251 |
+
</div>
|
| 252 |
+
""", unsafe_allow_html=True)
|
| 253 |
+
else:
|
| 254 |
+
st.markdown(f"""
|
| 255 |
+
<div class="chat-message assistant">
|
| 256 |
+
<img src="https://api.dicebear.com/7.x/bottts/svg?seed=assistant" class="avatar" alt="assistant">
|
| 257 |
+
<div class="message">{message["content"]}</div>
|
| 258 |
+
</div>
|
| 259 |
+
""", unsafe_allow_html=True)
|
| 260 |
+
|
| 261 |
+
# Show thinking animation
|
| 262 |
+
if st.session_state.thinking:
|
| 263 |
+
st.markdown(f"""
|
| 264 |
+
<div class="chat-message assistant">
|
| 265 |
+
<img src="https://api.dicebear.com/7.x/bottts/svg?seed=assistant" class="avatar" alt="assistant">
|
| 266 |
+
<div class="message">
|
| 267 |
+
<p>Thinking...</p>
|
| 268 |
+
<div class="thinking-animation">
|
| 269 |
+
<div></div><div></div><div></div><div></div>
|
| 270 |
+
</div>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
""", unsafe_allow_html=True)
|
| 274 |
+
|
| 275 |
+
try:
|
| 276 |
+
# Set API keys from session state
|
| 277 |
+
os.environ["OPENAI_API_KEY"] = st.session_state.openai_api_key
|
| 278 |
+
os.environ["TAVILY_API_KEY"] = st.session_state.tavily_api_key
|
| 279 |
+
|
| 280 |
+
# Get the last user message
|
| 281 |
+
last_user_message = next((msg["content"] for msg in reversed(st.session_state.messages)
|
| 282 |
+
if msg["role"] == "user"), None)
|
| 283 |
+
|
| 284 |
+
if last_user_message:
|
| 285 |
+
# Get model and max results from session state
|
| 286 |
+
model_name = st.session_state.get("model_name", "gpt-3.5-turbo-0125")
|
| 287 |
+
max_results = st.session_state.get("max_results", 3)
|
| 288 |
+
|
| 289 |
+
# Initialize the model and tools
|
| 290 |
+
chat_model = ChatOpenAI(model=model_name)
|
| 291 |
+
search = TavilySearchResults(max_results=max_results)
|
| 292 |
+
tools = [search]
|
| 293 |
+
|
| 294 |
+
# Create the agent
|
| 295 |
+
agent_executor = create_react_agent(chat_model, tools)
|
| 296 |
+
|
| 297 |
+
# Execute the agent
|
| 298 |
+
response = agent_executor.invoke({"messages": [HumanMessage(content=last_user_message)]})
|
| 299 |
+
|
| 300 |
+
# Extract the final AI response
|
| 301 |
+
ai_message = response['messages'][-1].content
|
| 302 |
+
|
| 303 |
+
# Add assistant response to chat history
|
| 304 |
+
st.session_state.messages.append({"role": "assistant", "content": ai_message})
|
| 305 |
+
|
| 306 |
+
except Exception as e:
|
| 307 |
+
# Add error message to chat history
|
| 308 |
+
error_message = f"Sorry, I encountered an error: {str(e)}"
|
| 309 |
+
st.session_state.messages.append({"role": "assistant", "content": error_message})
|
| 310 |
+
|
| 311 |
+
# Turn off thinking animation
|
| 312 |
+
st.session_state.thinking = False
|
| 313 |
+
|
| 314 |
+
# Force a rerun to update the chat with the response
|
| 315 |
+
st.experimental_rerun()
|
| 316 |
+
|
| 317 |
+
# Clear conversation when button is clicked
|
| 318 |
+
if clear_button:
|
| 319 |
+
st.session_state.messages = []
|
| 320 |
+
st.experimental_rerun()
|
| 321 |
+
|
| 322 |
+
else:
|
| 323 |
+
# Welcome message if API keys are not yet provided
|
| 324 |
+
st.info("👈 Please enter your API keys in the Settings tab to get started")
|
| 325 |
+
|
| 326 |
+
# Example of what the app can do
|
| 327 |
+
st.markdown('<h3 class="sub-header">What can this AI assistant do?</h3>', unsafe_allow_html=True)
|
| 328 |
+
|
| 329 |
+
# Feature cards
|
| 330 |
+
col1, col2 = st.columns(2)
|
| 331 |
+
with col1:
|
| 332 |
+
st.markdown("""
|
| 333 |
+
<div style="padding: 20px; border-radius: 10px; border: 1px solid #ddd; height: 200px;">
|
| 334 |
+
<h4>🌐 Real-time Web Search</h4>
|
| 335 |
+
<p>Get up-to-date information from across the internet on any topic.</p>
|
| 336 |
+
<p>The assistant uses Tavily's powerful search API to find relevant and current information.</p>
|
| 337 |
+
</div>
|
| 338 |
+
""", unsafe_allow_html=True)
|
| 339 |
+
|
| 340 |
+
st.markdown("""
|
| 341 |
+
<div style="padding: 20px; border-radius: 10px; border: 1px solid #ddd; margin-top: 20px; height: 200px;">
|
| 342 |
+
<h4>🧠 Powered by Advanced AI</h4>
|
| 343 |
+
<p>Utilizes OpenAI's powerful language models to understand questions and generate helpful responses.</p>
|
| 344 |
+
<p>Choose between different models based on your needs.</p>
|
| 345 |
+
</div>
|
| 346 |
+
""", unsafe_allow_html=True)
|
| 347 |
+
|
| 348 |
+
with col2:
|
| 349 |
+
st.markdown("""
|
| 350 |
+
<div style="padding: 20px; border-radius: 10px; border: 1px solid #ddd; height: 200px;">
|
| 351 |
+
<h4>💬 Natural Conversation</h4>
|
| 352 |
+
<p>Have a flowing conversation with follow-up questions and contextual responses.</p>
|
| 353 |
+
<p>The chat history is maintained throughout your session.</p>
|
| 354 |
+
</div>
|
| 355 |
+
""", unsafe_allow_html=True)
|
| 356 |
+
|
| 357 |
+
st.markdown("""
|
| 358 |
+
<div style="padding: 20px; border-radius: 10px; border: 1px solid #ddd; margin-top: 20px; height: 200px;">
|
| 359 |
+
<h4>📊 Customizable Results</h4>
|
| 360 |
+
<p>Adjust the number of search results to balance between comprehensive information and response speed.</p>
|
| 361 |
+
<p>Configure the AI model to suit your specific needs.</p>
|
| 362 |
+
</div>
|
| 363 |
+
""", unsafe_allow_html=True)
|
| 364 |
+
|
| 365 |
+
with tabs[1]: # About Tab
|
| 366 |
+
st.markdown('<h3 class="sub-header">About AI Search Assistant</h3>', unsafe_allow_html=True)
|
| 367 |
+
|
| 368 |
+
st.markdown("""
|
| 369 |
+
This application combines the power of large language models with real-time web search capabilities to provide you with up-to-date information on any topic.
|
| 370 |
+
|
| 371 |
+
### How It Works
|
| 372 |
+
|
| 373 |
+
1. **User Input**: You ask a question or request information on any topic
|
| 374 |
+
2. **Web Search**: The app searches the internet using Tavily's search API
|
| 375 |
+
3. **AI Processing**: OpenAI's language model processes the search results
|
| 376 |
+
4. **Response Generation**: The AI generates a comprehensive, informative response
|
| 377 |
+
|
| 378 |
+
### Technologies Used
|
| 379 |
+
|
| 380 |
+
- **Frontend**: Streamlit
|
| 381 |
+
- **AI**: OpenAI GPT models
|
| 382 |
+
- **Search**: Tavily Search API
|
| 383 |
+
- **Framework**: LangChain and LangGraph
|
| 384 |
+
|
| 385 |
+
### Privacy & Security
|
| 386 |
+
|
| 387 |
+
- Your API keys are stored only in your browser's session
|
| 388 |
+
- Keys are never saved to our servers
|
| 389 |
+
- Each user must provide their own API keys
|
| 390 |
+
""")
|
| 391 |
+
|
| 392 |
+
# Example use cases
|
| 393 |
+
st.markdown('<h3 class="sub-header">Example Use Cases</h3>', unsafe_allow_html=True)
|
| 394 |
+
|
| 395 |
+
use_cases = [
|
| 396 |
+
{
|
| 397 |
+
"title": "Research Assistant",
|
| 398 |
+
"description": "Get summaries and insights on academic topics, current events, or historical information.",
|
| 399 |
+
"example": "What are the latest developments in quantum computing?"
|
| 400 |
+
},
|
| 401 |
+
{
|
| 402 |
+
"title": "Current Events",
|
| 403 |
+
"description": "Stay updated on news, sports, entertainment, and global happenings.",
|
| 404 |
+
"example": "What major events happened this week in technology?"
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
"title": "Learning Tool",
|
| 408 |
+
"description": "Explain complex concepts in an easy-to-understand manner.",
|
| 409 |
+
"example": "Explain machine learning algorithms to a beginner."
|
| 410 |
+
},
|
| 411 |
+
{
|
| 412 |
+
"title": "Travel Planning",
|
| 413 |
+
"description": "Get information about destinations, attractions, and travel tips.",
|
| 414 |
+
"example": "What are the must-visit places in Tokyo?"
|
| 415 |
+
}
|
| 416 |
+
]
|
| 417 |
+
|
| 418 |
+
cols = st.columns(2)
|
| 419 |
+
for i, use_case in enumerate(use_cases):
|
| 420 |
+
with cols[i % 2]:
|
| 421 |
+
st.markdown(f"""
|
| 422 |
+
<div style="padding: 20px; border-radius: 10px; border: 1px solid #ddd; margin-bottom: 20px;">
|
| 423 |
+
<h4>{use_case['title']}</h4>
|
| 424 |
+
<p>{use_case['description']}</p>
|
| 425 |
+
<p><em>Example: "{use_case['example']}"</em></p>
|
| 426 |
+
</div>
|
| 427 |
+
""", unsafe_allow_html=True)
|
| 428 |
+
|
| 429 |
+
with tabs[2]: # Settings Tab
|
| 430 |
+
st.markdown('<h3 class="sub-header">API Configuration</h3>', unsafe_allow_html=True)
|
| 431 |
+
|
| 432 |
+
# API key input section with better UX
|
| 433 |
+
with st.form("api_form", clear_on_submit=False):
|
| 434 |
+
st.markdown("""
|
| 435 |
+
To use this application, you need to provide your own API keys for OpenAI and Tavily.
|
| 436 |
+
These keys are stored only in your browser session and are never saved on our servers.
|
| 437 |
+
""")
|
| 438 |
+
|
| 439 |
+
# Get API keys from session state or user input
|
| 440 |
+
openai_api_key = st.text_input(
|
| 441 |
+
"OpenAI API Key",
|
| 442 |
+
value=st.session_state.openai_api_key,
|
| 443 |
+
type="password",
|
| 444 |
+
help="Get your API key from https://platform.openai.com/api-keys"
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
tavily_api_key = st.text_input(
|
| 448 |
+
"Tavily API Key",
|
| 449 |
+
value=st.session_state.tavily_api_key,
|
| 450 |
+
type="password",
|
| 451 |
+
help="Get your API key from https://tavily.com/#api"
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
col1, col2 = st.columns([1, 3])
|
| 455 |
+
with col1:
|
| 456 |
+
submitted = st.form_submit_button("Save API Keys", use_container_width=True)
|
| 457 |
+
|
| 458 |
+
if submitted:
|
| 459 |
+
if not openai_api_key or not tavily_api_key:
|
| 460 |
+
st.error("Please provide both API keys")
|
| 461 |
+
else:
|
| 462 |
+
# Save API keys to session state
|
| 463 |
+
st.session_state.openai_api_key = openai_api_key
|
| 464 |
+
st.session_state.tavily_api_key = tavily_api_key
|
| 465 |
+
st.session_state.api_keys_valid = True
|
| 466 |
+
st.success("✅ API keys saved successfully!")
|
| 467 |
+
|
| 468 |
+
# Only show these settings if API keys are provided
|
| 469 |
+
if st.session_state.api_keys_valid:
|
| 470 |
+
st.markdown('<h3 class="sub-header">Search Settings</h3>', unsafe_allow_html=True)
|
| 471 |
+
|
| 472 |
+
col1, col2 = st.columns(2)
|
| 473 |
+
|
| 474 |
+
with col1:
|
| 475 |
+
# Model selection
|
| 476 |
+
model_options = {
|
| 477 |
+
"gpt-3.5-turbo-0125": "GPT-3.5 Turbo (Faster, Lower Cost)",
|
| 478 |
+
"gpt-4-turbo-preview": "GPT-4 Turbo (More Capable, Higher Cost)"
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
selected_model = st.selectbox(
|
| 482 |
+
"Select AI Model",
|
| 483 |
+
options=list(model_options.keys()),
|
| 484 |
+
format_func=lambda x: model_options[x],
|
| 485 |
+
index=0,
|
| 486 |
+
help="GPT-4 provides better results but costs more"
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
# Save to session state
|
| 490 |
+
st.session_state.model_name = selected_model
|
| 491 |
+
|
| 492 |
+
with col2:
|
| 493 |
+
# Number of search results
|
| 494 |
+
max_results = st.slider(
|
| 495 |
+
"Maximum Search Results",
|
| 496 |
+
min_value=1,
|
| 497 |
+
max_value=10,
|
| 498 |
+
value=st.session_state.get("max_results", 3),
|
| 499 |
+
help="More results provide more context but may slow down the response"
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
# Save to session state
|
| 503 |
+
st.session_state.max_results = max_results
|
| 504 |
+
|
| 505 |
+
# Information section
|
| 506 |
+
st.markdown('<h3 class="sub-header">How to Get API Keys</h3>', unsafe_allow_html=True)
|
| 507 |
+
|
| 508 |
+
col1, col2 = st.columns(2)
|
| 509 |
+
|
| 510 |
+
with col1:
|
| 511 |
+
st.markdown("""
|
| 512 |
+
<div style="padding: 20px; border-radius: 10px; border: 1px solid #ddd;">
|
| 513 |
+
<h4>OpenAI API Key</h4>
|
| 514 |
+
<ol>
|
| 515 |
+
<li>Go to <a href="https://platform.openai.com/signup" target="_blank">OpenAI</a> and create an account</li>
|
| 516 |
+
<li>Navigate to the API section</li>
|
| 517 |
+
<li>Click on "Create new secret key"</li>
|
| 518 |
+
<li>Copy the key and paste it in the form above</li>
|
| 519 |
+
</ol>
|
| 520 |
+
<a href="https://platform.openai.com/api-keys" target="_blank" class="stButton">
|
| 521 |
+
<button style="background-color: #1E88E5; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer;">
|
| 522 |
+
Get OpenAI API Key
|
| 523 |
+
</button>
|
| 524 |
+
</a>
|
| 525 |
+
</div>
|
| 526 |
+
""", unsafe_allow_html=True)
|
| 527 |
+
|
| 528 |
+
with col2:
|
| 529 |
+
st.markdown("""
|
| 530 |
+
<div style="padding: 20px; border-radius: 10px; border: 1px solid #ddd;">
|
| 531 |
+
<h4>Tavily API Key</h4>
|
| 532 |
+
<ol>
|
| 533 |
+
<li>Go to <a href="https://tavily.com/#api" target="_blank">Tavily</a> and create an account</li>
|
| 534 |
+
<li>Navigate to the API dashboard</li>
|
| 535 |
+
<li>Generate a new API key</li>
|
| 536 |
+
<li>Copy the key and paste it in the form above</li>
|
| 537 |
+
</ol>
|
| 538 |
+
<a href="https://tavily.com/#api" target="_blank" class="stButton">
|
| 539 |
+
<button style="background-color: #1E88E5; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer;">
|
| 540 |
+
Get Tavily API Key
|
| 541 |
+
</button>
|
| 542 |
+
</a>
|
| 543 |
+
</div>
|
| 544 |
+
""", unsafe_allow_html=True)
|
| 545 |
+
|
| 546 |
+
# Footer
|
| 547 |
+
st.markdown("""
|
| 548 |
+
<div class="footer">
|
| 549 |
+
<p>Built with ❤️ using Streamlit, LangChain and OpenAI | © 2025 AI Search Assistant (Made by Mahfujul Karim) </p>
|
| 550 |
+
</div>
|
| 551 |
+
""", unsafe_allow_html=True)
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
app:
|
| 5 |
+
build: .
|
| 6 |
+
ports:
|
| 7 |
+
- "8501:8501"
|
| 8 |
+
volumes:
|
| 9 |
+
- .:/app
|
| 10 |
+
environment:
|
| 11 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 12 |
+
- TAVILY_API_KEY=${TAVILY_API_KEY}
|
requirements.txt
CHANGED
|
@@ -1,3 +1,8 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
python-dotenv==1.0.1
|
| 2 |
+
streamlit==1.32.0
|
| 3 |
+
langchain-core>=0.2.22,<0.3.0
|
| 4 |
+
langchain==0.2.10
|
| 5 |
+
langchain-openai==0.1.17
|
| 6 |
+
langgraph==0.1.19
|
| 7 |
+
tavily-python>=0.3.1
|
| 8 |
+
tiktoken>=0.7.0
|
test.ipynb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"id": "a2502a91",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [
|
| 9 |
+
{
|
| 10 |
+
"name": "stdout",
|
| 11 |
+
"output_type": "stream",
|
| 12 |
+
"text": [
|
| 13 |
+
"[HumanMessage(content='Tell me the recent movies list in 2025', id='f421141e-8db1-4db4-9ebc-de1a46019386'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_lpcEZ355VWZjzZOOLsAF2zuA', 'function': {'arguments': '{\"query\":\"recent movies list 2025\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 91, 'total_tokens': 114, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-ef6c1818-38b5-4fd4-9a25-ef82fbe61c05-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'recent movies list 2025'}, 'id': 'call_lpcEZ355VWZjzZOOLsAF2zuA', 'type': 'tool_call'}], usage_metadata={'input_tokens': 91, 'output_tokens': 23, 'total_tokens': 114}), ToolMessage(content='[{\"url\": \"https://editorial.rottentomatoes.com/article/the-most-anticipated-movies-of-2025/\", \"content\": \"The Conjuring: Last Rites\\\\n(2025)\\\\n\\\\nRelease Date: September 5, 2025 \\\\nDirector: Michael Chaves \\\\nStarring: Vera Farmiga, Patrick Wilson, Ben Hardy\\\\n\\\\nThe Conjuring: Last Rites, will be the final chapter that centers paranormal experts Ed and Lorraine Warren. Both Patrick Wilson and Vera Farmiga are set to reprise their roles as the married couple, and they will be joined by a host of new cast members including X Men: Apocalypse’s Ben Hardy and Mia Tomlinson whose roles remain unknown. [...] The Lonely Island’s Akiva Schaffer brings back the bumbling detective Frank Drebin, originally played by Leslie Nielsen in the classic TV comedy Police Squad! and a trio of Naked Gun films from the late 1980s to mid-1990s. The man attempting to fill Nielsen’s shoes in the role? None other than Liam Neeson.\\\\n\\\\nFreakier Friday\\\\n(2025)\\\\n\\\\nFreakier Friday\\\\n(2025)\\\\n\\\\nRelease Date: August 8, 2025 \\\\nDirectors: Nisha Ganatra \\\\nStarring: Jamie Lee Curtis, Julia Butler, Chad Michael Murray [...] Good Fortune\\\\n(2025)\\\\n\\\\nGood Fortune\\\\n(2025)\\\\n\\\\nRelease Date: October 17, 2025 \\\\nDirectors: Aziz Ansari \\\\nStarring: Keanu Reeves, Keke Palmer, Seth Rogen\\\\n\\\\nAziz Ansari, known for Parks and Recreation and Master of None, has a new comedy on the horizon. The latest film follows the story of an angel who inhabits the body of a man’s boss to teach him a lesson. Cast members include Sandra Oh, Seth Rogen, Keanu Reeves as Ansari himself who will also serve as a producer on the film.\"}, {\"url\": \"https://variety.com/lists/best-movies-of-2025-so-far/\", \"content\": \"may all sound a bit overloaded, but “Bring Her Back” lurches forward with the warped psychedelic logic of a wounded dream. In their second feature, the Australian YouTube-horror-comedy-pranksters-turned-filmmakers Danny and Michael Philippou (“Talk to Me”) find terrifying ways to get under your skin, pushing everything to the brink of transgression, using domestic trauma to create a symphonic projection of Munchausen syndrome by proxy, all sealed by Hawkins’ gargoyle grin of evil. \\xad—OG [...] ## Mission: Impossible — The Final Reckoning\\\\n\\\\nMISSION: IMPOSSIBLE – THE FINAL RECKONING, (aka MISSION: IMPOSSIBLE 8), Tom Cruise, 2025. © Paramount Pictures / Courtesy Everett Collection\\\\nMISSION: IMPOSSIBLE – THE FINAL RECKONING, (aka MISSION: IMPOSSIBLE 8), Tom Cruise, 2025. © Paramount Pictures / Courtesy Everett Collection [...] intelligent and ruthlessly efficient thriller. Speaking of Bond, Pierce Brosnan pops up as the couple’s boss in a movie that delivers all the intrigue you’d expect from the genre, in addition to a perceptive case study on how successful marriages work: A little secrecy keeps things spicy, so long as both parties can fully trust one another. Violate that, and you stand to destroy not just the union, but the entire Western World. —Peter Debruge\"}, {\"url\": \"https://deadline.com/lists/2025-movies/\", \"content\": \"On the heels of a packed holiday box office at the end of 2024 that saw Wicked: Part One versus Gladiator II followed by Moana 2 ahead of Thanksgiving and then A Complete Unknown and Babygirl leading the Christmas Day releases, 2025 has its own stacked roster of big film titles. [...] The latest Disney live-action remake of a princess film, starring Rachel Zegler (The Ballad of Songbirds and Snakes, West Side Story), arrives in theaters March 21, 2025. Gal Gadot co-stars as the Evil Queen, and of course the seven dwarves Grumpy, Sneezy, Dopey, Sleepy, Happy, Bashful and Doc will be present as well.\\\\n\\\\nRELATED: ‘Snow White’ Trailer Features Gal Gadot’s Evil Queen, Rachel Zegler, The Seven Dwarfs & More [...] Starring Chris Pratt, Millie Bobby Brown and more, the latest film from the Russo brothers arrives on Netflix March 14, 2025. The film, written by Avengersscribes Christopher Markus & Stephen McFeely, is adapted from Simon Stålenhag’s illustrated novel, set in the 1990s where sentient robots live in exile after a failed uprising.\\\\n\\\\nRELATED: ‘The Electric State’ Trailer: Millie Bobby Brown, Chris Pratt, The Russos And Robots\"}]', name='tavily_search_results_json', id='5f046ab3-63fe-4fc0-bc31-50dc91b7eacc', tool_call_id='call_lpcEZ355VWZjzZOOLsAF2zuA'), AIMessage(content='Here are some of the recent movies in 2025:\\n\\n1. **The Conjuring: Last Rites**\\n - **Release Date:** September 5, 2025\\n - **Director:** Michael Chaves\\n - **Starring:** Vera Farmiga, Patrick Wilson, Ben Hardy\\n - **Description:** The final chapter in the series focusing on paranormal experts Ed and Lorraine Warren.\\n\\n2. **Freakier Friday**\\n - **Release Date:** August 8, 2025\\n - **Director:** Nisha Ganatra\\n - **Starring:** Jamie Lee Curtis, Julia Butler, Chad Michael Murray\\n - **Description:** A new twist on the classic \"Freaky Friday\" story.\\n\\n3. **Good Fortune**\\n - **Release Date:** October 17, 2025\\n - **Director:** Aziz Ansari\\n - **Starring:** Keanu Reeves, Keke Palmer, Seth Rogen\\n - **Description:** A comedy about an angel inhabiting a man\\'s boss to teach him a lesson.\\n\\n4. **Mission: Impossible — The Final Reckoning**\\n - **Description:** The eighth installment in the Mission: Impossible series.\\n\\n5. **Snow White**\\n - **Release Date:** March 21, 2025\\n - **Starring:** Rachel Zegler, Gal Gadot\\n - **Description:** A Disney live-action remake of the classic Snow White story.\\n\\n6. **The Electric State**\\n - **Release Date:** March 14, 2025\\n - **Starring:** Chris Pratt, Millie Bobby Brown\\n - **Description:** A film about sentient robots living in exile after a failed uprising.\\n\\nYou can find more details about these movies in the following links:\\n1. [Rotten Tomatoes - Most Anticipated Movies of 2025](https://editorial.rottentomatoes.com/article/the-most-anticipated-movies-of-2025/)\\n2. [Variety - Best Movies of 2025 So Far](https://variety.com/lists/best-movies-of-2025-so-far/)\\n3. [Deadline - 2025 Movies](https://deadline.com/lists/2025-movies/)', response_metadata={'token_usage': {'completion_tokens': 463, 'prompt_tokens': 1172, 'total_tokens': 1635, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-fc16df9c-a29e-4599-a1ad-463f749cc6ff-0', usage_metadata={'input_tokens': 1172, 'output_tokens': 463, 'total_tokens': 1635})]\n"
|
| 14 |
+
]
|
| 15 |
+
}
|
| 16 |
+
],
|
| 17 |
+
"source": [
|
| 18 |
+
"import os\n",
|
| 19 |
+
"from dotenv import load_dotenv, find_dotenv\n",
|
| 20 |
+
"_ = load_dotenv(find_dotenv())\n",
|
| 21 |
+
"from langchain_openai import ChatOpenAI\n",
|
| 22 |
+
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
|
| 23 |
+
"from langgraph.prebuilt import create_react_agent\n",
|
| 24 |
+
"from langchain_core.messages import HumanMessage\n",
|
| 25 |
+
"\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"\n",
|
| 28 |
+
"# Access the variables\n",
|
| 29 |
+
"OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')\n",
|
| 30 |
+
"TAVILY_API_KEY = os.getenv('TAVILY_API_KEY')\n",
|
| 31 |
+
"\n",
|
| 32 |
+
"os.environ[\"OPENAI_API_KEY\"] = OPENAI_API_KEY\n",
|
| 33 |
+
"os.environ[\"TAVILY_API_KEY\"] = TAVILY_API_KEY\n",
|
| 34 |
+
"\n",
|
| 35 |
+
"\n",
|
| 36 |
+
"chatModel = ChatOpenAI(model=\"gpt-3.5-turbo-0125\")\n",
|
| 37 |
+
"search = TavilySearchResults(max_results=3)\n",
|
| 38 |
+
"\n",
|
| 39 |
+
"# res = search.invoke(\"Tell me the recent movies list in 2025\")\n",
|
| 40 |
+
"# print(res)\n",
|
| 41 |
+
"\n",
|
| 42 |
+
"tool = [search]\n",
|
| 43 |
+
"\n",
|
| 44 |
+
"agent_executor = create_react_agent(chatModel, tool)\n",
|
| 45 |
+
"\n",
|
| 46 |
+
"\n",
|
| 47 |
+
"response = agent_executor.invoke({\"messages\": [HumanMessage(content=\"Tell me the recent movies list in 2025\")]})\n",
|
| 48 |
+
"\n",
|
| 49 |
+
"print(response['messages'])"
|
| 50 |
+
]
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"cell_type": "code",
|
| 54 |
+
"execution_count": null,
|
| 55 |
+
"id": "c254fccd",
|
| 56 |
+
"metadata": {},
|
| 57 |
+
"outputs": [],
|
| 58 |
+
"source": []
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"cell_type": "code",
|
| 62 |
+
"execution_count": null,
|
| 63 |
+
"id": "eaa961c4",
|
| 64 |
+
"metadata": {},
|
| 65 |
+
"outputs": [],
|
| 66 |
+
"source": []
|
| 67 |
+
}
|
| 68 |
+
],
|
| 69 |
+
"metadata": {
|
| 70 |
+
"kernelspec": {
|
| 71 |
+
"display_name": "llmapp",
|
| 72 |
+
"language": "python",
|
| 73 |
+
"name": "python3"
|
| 74 |
+
},
|
| 75 |
+
"language_info": {
|
| 76 |
+
"codemirror_mode": {
|
| 77 |
+
"name": "ipython",
|
| 78 |
+
"version": 3
|
| 79 |
+
},
|
| 80 |
+
"file_extension": ".py",
|
| 81 |
+
"mimetype": "text/x-python",
|
| 82 |
+
"name": "python",
|
| 83 |
+
"nbconvert_exporter": "python",
|
| 84 |
+
"pygments_lexer": "ipython3",
|
| 85 |
+
"version": "3.11.13"
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"nbformat": 4,
|
| 89 |
+
"nbformat_minor": 5
|
| 90 |
+
}
|