Vivek0912 commited on
Commit
535dff0
·
1 Parent(s): 0662545

initial commits

Browse files
Files changed (2) hide show
  1. Dockerfile +1 -1
  2. src/chat_ui.py +315 -0
Dockerfile CHANGED
@@ -17,4 +17,4 @@ EXPOSE 8501
17
 
18
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
 
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
17
 
18
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
 
20
+ ENTRYPOINT ["streamlit", "run", "src/chat_ui.py", "--server.port=8501", "--server.address=0.0.0.0"]
src/chat_ui.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import streamlit as st
3
+ import requests
4
+ import pandas as pd
5
+ import base64
6
+ import logging
7
+ import os
8
+
9
+
10
+ # ===============================
11
+ # Config: Dynamic API_URL based on environment
12
+ # ===============================
13
+
14
+ # Read API base URL and endpoint from environment variables
15
+ api_base_url = os.getenv("API_BASE_URL", "http://127.0.0.1:8000")
16
+ api_endpoint = os.getenv("API_ENDPOINT", "/api/process-text")
17
+ # Ensure no double slashes
18
+ API_URL = api_base_url.rstrip("/") + "/" + api_endpoint.lstrip("/")
19
+
20
+ st.set_page_config(page_title="AI Database Assistant", layout="wide")
21
+
22
+ # Show current API URL in sidebar for debugging
23
+ st.sidebar.info(f"**API URL:** {API_URL}")
24
+
25
+ # Setup logger
26
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
27
+
28
+ st.title("💬 AI Database Assistant")
29
+ st.write("Ask questions in plain English to generate and run SQL queries.")
30
+ # st.markdown(
31
+ # "<h1 style='text-align: center; color: white; background: rgba(0,0,0,0.6); "
32
+ # "padding: 12px; border-radius: 10px;'>🤖 AI Database Assistant</h1>",
33
+ # unsafe_allow_html=True,
34
+ # )
35
+ # st.write("Ask questions in plain English to generate and run SQL queries.")
36
+
37
+ # ===============================
38
+ # Sidebar Settings
39
+ # ===============================
40
+ st.sidebar.header("⚙️ Settings")
41
+
42
+ model = st.sidebar.selectbox("Select Model", ["gpt-3.5-turbo", "gpt-4", "gpt-4o-mini"])
43
+ agent = st.sidebar.selectbox("Select Agent", ["default", "sql-agent", "custom-agent"])
44
+ theme = st.sidebar.selectbox("Theme", ["Light", "Dark", "Custom"])
45
+
46
+ # ===============================
47
+ # Theme CSS
48
+ # ===============================
49
+ def inject_css(theme_choice: str):
50
+ if theme_choice == "Light":
51
+ css = """
52
+ body, .stApp { background: #f8fafc !important; }
53
+ .chat-container { max-width: 900px; margin: auto; }
54
+ .user-bubble {
55
+ background: linear-gradient(90deg, #e3f2fd, #ffffff);
56
+ color: #222;
57
+ float: right;
58
+ clear: both;
59
+ }
60
+ .ai-bubble {
61
+ background: linear-gradient(90deg, #f3e5f5, #ffffff);
62
+ color: #222;
63
+ float: left;
64
+ clear: both;
65
+ }
66
+ .error-bubble {
67
+ background: #ffebee; color: #b71c1c; font-weight: bold;
68
+ float: left; clear: both;
69
+ }
70
+ section[data-testid="stSidebar"] { background: #e3f2fd !important; }
71
+ """
72
+ elif theme_choice == "Dark":
73
+ css = """
74
+ body, .stApp { background: #181825 !important; }
75
+ .chat-container { max-width: 900px; margin: auto; }
76
+ .user-bubble {
77
+ background: linear-gradient(135deg, #3a0ca3, #4361ee);
78
+ color: #f8fafc;
79
+ float: right;
80
+ clear: both;
81
+ }
82
+ .ai-bubble {
83
+ background: linear-gradient(135deg, #14213d, #1d3557);
84
+ color: #f8fafc;
85
+ float: left;
86
+ clear: both;
87
+ }
88
+ .error-bubble {
89
+ background: #ffb4ab; color: #b71c1c; font-weight: bold;
90
+ float: left; clear: both;
91
+ }
92
+ section[data-testid="stSidebar"] { background: #232946 !important; }
93
+ """
94
+ else: # Custom theme
95
+ css = """
96
+ body, .stApp { background: #fffbe7 !important; }
97
+ .chat-container { max-width: 900px; margin: auto; }
98
+ .user-bubble {
99
+ background: linear-gradient(90deg, #ffe082, #fffbe7);
100
+ color: #6d4c00;
101
+ float: right;
102
+ clear: both;
103
+ }
104
+ .ai-bubble {
105
+ background: linear-gradient(90deg, #b2dfdb, #fffbe7);
106
+ color: #004d40;
107
+ float: left;
108
+ clear: both;
109
+ }
110
+ .error-bubble {
111
+ background: #ffe0b2; color: #b71c1c; font-weight: bold;
112
+ float: left; clear: both;
113
+ }
114
+ section[data-testid="stSidebar"] { background: #ffe082 !important; }
115
+ """
116
+
117
+ # Common bubble styling
118
+ css += """
119
+ .user-bubble, .ai-bubble, .error-bubble {
120
+ border-radius: 14px;
121
+ padding: 10px 14px;
122
+ margin: 6px 0;
123
+ display: inline-block;
124
+ max-width: 75%;
125
+ word-wrap: break-word;
126
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25);
127
+ }
128
+ """
129
+ st.markdown(f"<style>{css}</style>", unsafe_allow_html=True)
130
+
131
+ inject_css(theme)
132
+
133
+ # ===============================
134
+ # Avatar Setup
135
+ # ===============================
136
+ # Default base64 encoded avatars
137
+ DEFAULT_USER_AVATAR = """
138
+ data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTAgM2MyLjY3IDAgOC0xLjMzIDgtM3Y0LjE1YzAgMS42My0zLjMzIDIuODUtOCAyLjg1cy04LTEuMjItOC0yLjg1VjJjMCAxLjY3IDUuMzMgMyA4IDN6bTAgMTFjLTIuNjcgMC04LTEuMzMtOC0zdjQuMTVjMCAxLjYzIDMuMzMgMi44NSA4IDIuODVzOC0xLjIyIDgtMi44NVYxM2MwIDEuNjctNS4zMyAzLTggM3oiIGZpbGw9IiM0Q0FGNTAiLz48L3N2Zz4=
139
+ """
140
+
141
+ DEFAULT_AI_AVATAR = """
142
+ data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0xIDE2LjkydjEuMDhjMCAuNTUuNDUgMSAxIDFoMmMuNTUgMCAxLS40NSAxLTF2LTEuMDhjLS42My4xMy0xLjI5LjItMS45Ny4yLS43IDAtMS4zNC0uMDctMS45Ny0uMnptNS43OC0yLjc0QzE1LjI0IDE3LjM0IDEzLjY3IDE4IDEyIDE4cy0zLjI0LS42Ni00Ljc4LTEuODJDNC40NCAxNC4zNCAzIDEyLjA0IDMgOS41IDMgNS45MSA1LjkxIDMgOS41IDNoNUM2LjY1IDMgNCA0LjY1IDQgNi43MWMwIDIuMDYgMS42NyAzLjc0IDMuNzMgMy43NC4zNiAwIC43MS0uMDUgMS4wNS0uMTUgMS4xIDEuMTEgMi41NyAxLjc5IDQuMjIgMS43OSAxLjY0IDAgMy4xMi0uNjkgNC4yMi0xLjc5LjMzLjEuNjkuMTUgMS4wNS4xNSAyLjA2IDAgMy43My0xLjY4IDMuNzMtMy43NCAwLTIuMDYtMS42Ny0zLjcxLTMuNzMtMy43MWgtNUM3LjkxIDMgNSA1LjkxIDUgOS41YzAgMi41NCAxLjQ0IDQuODQgMy43OCA2LjY4eiIgZmlsbD0iIzI5NzlGRiIvPjwvc3ZnPg==
143
+ """
144
+
145
+ # ===============================
146
+ # Session State
147
+ # ===============================
148
+ if "messages" not in st.session_state:
149
+ st.session_state["messages"] = []
150
+
151
+ # ===============================
152
+ # Helper: Render Chat History
153
+ # ===============================
154
+ def render_chat():
155
+ st.markdown("<div class='chat-container'>", unsafe_allow_html=True)
156
+ for msg in st.session_state["messages"]:
157
+ if msg["role"] == "user":
158
+ st.markdown(f'<div class="user-bubble">{msg["content"]}</div>', unsafe_allow_html=True)
159
+ elif msg["role"] == "assistant":
160
+ bubble_class = "error-bubble" if msg.get("is_error") else "ai-bubble"
161
+ st.markdown(f'<div class="{bubble_class}">{msg["content"]}</div>', unsafe_allow_html=True)
162
+
163
+ if msg.get("data"):
164
+ df = pd.DataFrame(msg["data"])
165
+ st.dataframe(df, use_container_width=True)
166
+
167
+ # Add download button for results
168
+ csv = df.to_csv(index=False).encode("utf-8")
169
+ st.download_button("📥 Download CSV", csv, "results.csv", "text/csv")
170
+
171
+ if msg.get("chart"):
172
+ img_data = base64.b64decode(msg["chart"])
173
+ st.image(img_data, use_column_width=True)
174
+ st.markdown("</div>", unsafe_allow_html=True)
175
+
176
+
177
+ def render_chat():
178
+ # Spinner CSS (only inject once)
179
+ st.markdown('''
180
+ <style>
181
+ .spinner {
182
+ display: inline-block;
183
+ width: 1.3em;
184
+ height: 1.3em;
185
+ border: 3px solid #e0e0e0;
186
+ border-top: 3px solid #2193b0;
187
+ border-radius: 50%;
188
+ animation: spin 0.8s linear infinite;
189
+ margin-right: 0.7em;
190
+ }
191
+ @keyframes spin {
192
+ 0% { transform: rotate(0deg); }
193
+ 100% { transform: rotate(360deg); }
194
+ }
195
+ </style>
196
+ ''', unsafe_allow_html=True)
197
+ st.markdown("<div class='chat-container'>", unsafe_allow_html=True)
198
+ user_avatar = DEFAULT_USER_AVATAR
199
+ ai_avatar = DEFAULT_AI_AVATAR
200
+ for msg in st.session_state["messages"]:
201
+ if msg["role"] == "user":
202
+ st.markdown(
203
+ f'''<div style="display: flex; align-items: flex-start; justify-content: flex-end; margin-bottom: 0.5em;">
204
+ <div style="margin-right: 0.5em;">
205
+ <img src="{user_avatar}" alt="User" style="width: 2.3rem; height: 2.3rem; border-radius: 50%; border: 2px solid #e3f2fd; background: #fff; object-fit: cover;" />
206
+ </div>
207
+ <div class="user-bubble">{msg["content"]}</div>
208
+ </div>''',
209
+ unsafe_allow_html=True)
210
+ elif msg["role"] == "assistant":
211
+ bubble_class = "error-bubble" if msg.get("is_error") else "ai-bubble"
212
+ # Show spinner if this is a placeholder 'Thinking...' message
213
+ if msg.get("is_placeholder"):
214
+ st.markdown(
215
+ f'''<div style="display: flex; align-items: flex-start; margin-bottom: 0.5em;">
216
+ <div style="margin-right: 0.5em;">
217
+ <img src="{ai_avatar_url}" alt="AI" style="width: 2.3rem; height: 2.3rem; border-radius: 50%; border: 2px solid #b2dfdb; background: #fff; object-fit: cover;" />
218
+ </div>
219
+ <div class="{bubble_class}"><span class="spinner"></span>Thinking...</div>
220
+ </div>''',
221
+ unsafe_allow_html=True)
222
+ else:
223
+ st.markdown(
224
+ f'''<div style="display: flex; align-items: flex-start; margin-bottom: 0.5em;">
225
+ <div style="margin-right: 0.5em;">
226
+ <img src="{ai_avatar_url}" alt="AI" style="width: 2.3rem; height: 2.3rem; border-radius: 50%; border: 2px solid #b2dfdb; background: #fff; object-fit: cover;" />
227
+ </div>
228
+ <div class="{bubble_class}">{msg["content"]}</div>
229
+ </div>''',
230
+ unsafe_allow_html=True)
231
+
232
+ if msg.get("data"):
233
+ df = pd.DataFrame(msg["data"])
234
+ st.dataframe(df, use_container_width=True)
235
+
236
+ # Add download button for results
237
+ csv = df.to_csv(index=False).encode("utf-8")
238
+ st.download_button("\U0001F4C5 Download CSV", csv, "results.csv", "text/csv")
239
+
240
+ if msg.get("chart"):
241
+ img_data = base64.b64decode(msg["chart"])
242
+ st.image(img_data, use_column_width=True)
243
+ st.markdown("</div>", unsafe_allow_html=True)
244
+
245
+ render_chat()
246
+
247
+ # ===============================
248
+ # User Input
249
+ # ===============================
250
+
251
+ # Disable chat input while AI is working
252
+ pending = False
253
+ if st.session_state["messages"]: # Check if there are any messages
254
+ if st.session_state["messages"][-1]["role"] == "assistant":
255
+ pending = st.session_state["messages"][-1].get("is_placeholder", False)
256
+
257
+ user_query = st.chat_input(
258
+ "Ask me something about your database...",
259
+ disabled=pending or False # Ensure disabled is always boolean
260
+ )
261
+ if user_query and not pending:
262
+ st.session_state["messages"].append({"role": "user", "content": user_query})
263
+ st.session_state["messages"].append({"role": "assistant", "content": "🤔 Thinking...", "is_placeholder": True})
264
+ st.rerun()
265
+
266
+ # ===============================
267
+ # Handle Pending Assistant Response
268
+ # ===============================
269
+ if (
270
+ st.session_state["messages"]
271
+ and st.session_state["messages"][-1].get("is_placeholder")
272
+ and len(st.session_state["messages"]) >= 2
273
+ and st.session_state["messages"][-2]["role"] == "user"
274
+ ):
275
+ user_query = st.session_state["messages"][-2]["content"]
276
+
277
+ try:
278
+ with st.spinner("Working..."):
279
+ payload = {"question": user_query, "model": model, "agent": agent}
280
+ logging.info(f"Sending API request with: {payload}")
281
+ response = requests.post(API_URL, json=payload, timeout=60)
282
+
283
+ try:
284
+ result = response.json()
285
+ except ValueError:
286
+ result = {"detail": response.text}
287
+
288
+ if response.status_code != 200:
289
+ answer_text = f"❌ Error: {result.get('detail') or result.get('message') or response.text}"
290
+ rows, chart, is_error = [], None, True
291
+ elif "message" in result and not ("rows" in result or "chart" in result):
292
+ answer_text, rows, chart, is_error = result["message"], [], None, False
293
+ else:
294
+ rows = result.get("rows", [])
295
+ chart = result.get("chart", None)
296
+ if rows:
297
+ answer_text = f"Here are the top {len(rows)} results I found:"
298
+ else:
299
+ answer_text = "I could not find matching records for your query."
300
+ is_error = False
301
+
302
+ except Exception as e:
303
+ logging.error(f"Exception: {e}")
304
+ answer_text, rows, chart, is_error = f"⚠️ Exception: {str(e)}", [], None, True
305
+
306
+ # Replace placeholder with final assistant message
307
+ st.session_state["messages"][-1] = {
308
+ "role": "assistant",
309
+ "content": answer_text,
310
+ "data": rows,
311
+ "chart": chart,
312
+ "is_error": is_error,
313
+ }
314
+
315
+ st.rerun()