MD MAFJUJUL KARIM commited on
Commit
39fa096
·
1 Parent(s): f646ccc

Initial commit for Docker LangGraph app

Browse files
Files changed (9) hide show
  1. .env +2 -0
  2. Dockerfile +10 -13
  3. Procfile +1 -0
  4. README.md +150 -20
  5. agent.py +32 -0
  6. app.py +551 -0
  7. docker-compose.yml +12 -0
  8. requirements.txt +8 -3
  9. 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.9-slim
2
 
3
  WORKDIR /app
4
 
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- software-properties-common \
9
- git \
10
- && rm -rf /var/lib/apt/lists/*
11
 
12
- COPY requirements.txt ./
13
- COPY src/ ./src/
14
 
15
- RUN pip3 install -r requirements.txt
 
16
 
 
17
  EXPOSE 8501
18
 
19
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
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
- title: AI Agent Using LangGraph
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Streamlit template space
12
- license: mit
13
- ---
14
-
15
- # Welcome to Streamlit!
16
-
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
-
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
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
+ }