KingNish commited on
Commit
0f2dc9a
·
0 Parent(s):

Base Done

Browse files
Files changed (5) hide show
  1. .env.example +3 -0
  2. .gitignore +26 -0
  3. app.css +32 -0
  4. app.py +396 -0
  5. requirements.txt +3 -0
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ OPENAI_API_KEY=sk-your-api-key-here
2
+ OPENAI_BASE_URL=https://api.openai.com/v1
3
+ OPENAI_MODEL=gpt-3.5-turbo
.gitignore ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+
10
+ # Virtual environment
11
+ .venv/
12
+ venv/
13
+
14
+ # Environment variables
15
+ .env
16
+
17
+ # IDE
18
+ .vscode/
19
+ .idea/
20
+
21
+ # OS
22
+ .DS_Store
23
+ Thumbs.db
24
+
25
+ # Gradio (caches temp files)
26
+ gradio_cached_examples/
app.css ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .gradio-container, .main {
2
+ padding: 0 !important;
3
+ --layout-gap: 0 !important;
4
+ }
5
+
6
+ #main-row {
7
+ height: 100vh;
8
+ max-height: 1500px;
9
+ }
10
+
11
+ #sidebar {
12
+ height: 100%;
13
+ background-color: var(--block-background-fill);
14
+ padding: 12px;
15
+ border-right: 1px solid var(--border-color-primary);
16
+ overflow-y: auto;
17
+ }
18
+
19
+ #chat-column {
20
+ height: 100%;
21
+ display: flex;
22
+ flex-direction: column;
23
+ padding: 1px;
24
+ min-width: 0;
25
+ overflow: hidden;
26
+ }
27
+
28
+ #chatbot {
29
+ flex: 1;
30
+ min-height: 100px;
31
+ border: 0;
32
+ }
app.py ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import uuid
3
+ import gradio as gr
4
+ from openai import OpenAI
5
+ from dotenv import load_dotenv
6
+ import os
7
+
8
+ load_dotenv()
9
+
10
+ client = OpenAI(
11
+ api_key=os.getenv("OPENAI_API_KEY"),
12
+ base_url=os.getenv("OPENAI_BASE_URL"),
13
+ )
14
+ model = os.getenv("OPENAI_MODEL")
15
+
16
+
17
+ def _conv_choices(state_value):
18
+ return gr.update(
19
+ choices=[c["label"] for c in state_value["conversations"]],
20
+ value=next(
21
+ (c["label"] for c in state_value["conversations"]
22
+ if c["key"] == state_value.get("conversation_id")), None),
23
+ )
24
+
25
+
26
+ class GradioEvents:
27
+ """Event handlers for the chatbot UI."""
28
+
29
+ # ------------------------------------------------------------------ stream
30
+ @staticmethod
31
+ def stream_response(message, state_value):
32
+ """Stream a chat completion into the active conversation."""
33
+ if not message or not message.strip():
34
+ yield gr.skip()
35
+ return
36
+
37
+ # Create new conversation if there isn't an active one
38
+ if not state_value.get("conversation_id"):
39
+ conv_id = str(uuid.uuid4())
40
+ state_value["conversation_id"] = conv_id
41
+ state_value["conversations"].append(
42
+ {"label": message[:30], "key": conv_id})
43
+ state_value["conversation_contexts"][conv_id] = {
44
+ "history": []
45
+ }
46
+ else:
47
+ conv_id = state_value["conversation_id"]
48
+ ctx = state_value["conversation_contexts"].setdefault(
49
+ conv_id, {"history": []})
50
+
51
+ ctx = state_value["conversation_contexts"][conv_id]
52
+
53
+ # Update conversation label if blank
54
+ for c in state_value["conversations"]:
55
+ if c["key"] == conv_id and not c.get("label"):
56
+ c["label"] = message[:30]
57
+ break
58
+
59
+ # Append user message
60
+ ctx["history"].append({"role": "user", "content": message})
61
+
62
+ # Initial yield: clear input, show user message
63
+ yield { msg: gr.update(value=""), chatbot: gr.update(value=ctx["history"]), state: gr.update(value=state_value), conv_choice: _conv_choices(state_value), send_btn: gr.update(visible=False), stop_btn: gr.update(visible=True)}
64
+
65
+ reasoning_content = ""
66
+ normal_content = ""
67
+ reasoning_started = False
68
+ start_time = time.time()
69
+ partial = []
70
+ temp = []
71
+
72
+ try:
73
+ stream = client.chat.completions.create(
74
+ model=model,
75
+ messages=ctx["history"],
76
+ stream=True
77
+ )
78
+
79
+ for chunk in stream:
80
+ chunk = chunk.to_dict()
81
+ delta = chunk['choices'][0]['delta']
82
+
83
+ if delta.get('reasoning_content'):
84
+ reasoning_content += delta['reasoning_content']
85
+ if not reasoning_started:
86
+ start_time = time.time()
87
+ if normal_content:
88
+ partial.append({
89
+ "role": "assistant",
90
+ "content": normal_content,
91
+ "metadata": None,
92
+ "is_final": True,
93
+ })
94
+ normal_content = ""
95
+ reasoning_started = True
96
+ temp = [{
97
+ "role": "assistant",
98
+ "content": reasoning_content,
99
+ "metadata": {"title": "Thinking..."},
100
+ "is_final": False,
101
+ }]
102
+ elif delta.get('content', ''):
103
+ if reasoning_started:
104
+ reasoning_started = False
105
+ partial.append({
106
+ "role": "assistant",
107
+ "content": reasoning_content,
108
+ "metadata": {"title": "Thought for " + f"{time.time() - start_time:.2f}s"},
109
+ "is_final": True,
110
+ })
111
+ reasoning_content = ""
112
+ normal_content += delta['content']
113
+ temp = [{
114
+ "role": "assistant",
115
+ "content": normal_content,
116
+ "metadata": None,
117
+ "is_final": False,
118
+ }]
119
+
120
+ yield {
121
+ chatbot: gr.update(value=ctx["history"] + partial + temp),
122
+ state: gr.update(value=state_value),
123
+ }
124
+
125
+ # Persist assistant messages into history
126
+ ctx["history"].extend(partial)
127
+ if temp:
128
+ ctx["history"].extend(temp)
129
+ # Mark last assistant message as final
130
+ if ctx["history"] and ctx["history"][-1].get("role") == "assistant":
131
+ ctx["history"][-1]["is_final"] = True
132
+
133
+ yield {
134
+ chatbot: gr.update(value=ctx["history"]),
135
+ state: gr.update(value=state_value),
136
+ send_btn: gr.update(visible=True),
137
+ stop_btn: gr.update(visible=False),
138
+ }
139
+
140
+ except Exception as exc: # noqa: BLE001
141
+ print("model:", model, "-", "Error:", exc)
142
+ ctx["history"].append({
143
+ "role": "assistant",
144
+ "content": f'<span style="color: var(--color-red-500)">{exc}</span>',
145
+ })
146
+ yield {
147
+ chatbot: gr.update(value=ctx["history"]),
148
+ state: gr.update(value=state_value),
149
+ send_btn: gr.update(visible=True),
150
+ stop_btn: gr.update(visible=False),
151
+ }
152
+ raise
153
+
154
+ # ------------------------------------------------------------ conversations
155
+ @staticmethod
156
+ def new_chat(state_value):
157
+ state_value["conversation_id"] = ""
158
+ return (
159
+ gr.update(value=None),
160
+ gr.update(value=None),
161
+ gr.update(value=state_value),
162
+ )
163
+
164
+ @staticmethod
165
+ def select_conversation(choice, state_value):
166
+ if not choice:
167
+ return gr.skip()
168
+ conv_id = None
169
+ for c in state_value["conversations"]:
170
+ if c["label"] == choice:
171
+ conv_id = c["key"]
172
+ break
173
+ if not conv_id or conv_id == state_value.get("conversation_id"):
174
+ return gr.skip()
175
+
176
+ state_value["conversation_id"] = conv_id
177
+ ctx = state_value["conversation_contexts"].get(conv_id, {})
178
+ return (
179
+ gr.update(value=ctx.get("history", [])),
180
+ gr.update(value=state_value),
181
+ )
182
+
183
+ @staticmethod
184
+ def delete_selected_conversation(choice, state_value):
185
+ if not choice:
186
+ return gr.skip()
187
+ target_id = None
188
+ for c in state_value["conversations"]:
189
+ if c["label"] == choice:
190
+ target_id = c["key"]
191
+ break
192
+ if not target_id:
193
+ return gr.skip()
194
+
195
+ state_value["conversation_contexts"].pop(target_id, None)
196
+ state_value["conversations"] = [
197
+ c for c in state_value["conversations"] if c["key"] != target_id
198
+ ]
199
+ was_active = state_value.get("conversation_id") == target_id
200
+ if was_active:
201
+ state_value["conversation_id"] = ""
202
+ return (
203
+ _conv_choices(state_value),
204
+ gr.update(value=None),
205
+ gr.update(value=True),
206
+ gr.update(value=state_value),
207
+ )
208
+ return (
209
+ _conv_choices(state_value),
210
+ gr.skip(),
211
+ gr.skip(),
212
+ gr.skip(),
213
+ gr.update(value=state_value),
214
+ )
215
+
216
+ # ---------------------------------------------------------------- settings
217
+
218
+ @staticmethod
219
+ def clear_history(state_value):
220
+ if not state_value.get("conversation_id"):
221
+ return gr.skip()
222
+ state_value["conversation_contexts"][
223
+ state_value["conversation_id"]]["history"] = []
224
+ return gr.update(value=[]), gr.update(value=state_value)
225
+
226
+ @staticmethod
227
+ def cancel_stream(state_value):
228
+ """Mark the current assistant message as cancelled."""
229
+ if not state_value.get("conversation_id"):
230
+ return gr.skip()
231
+ ctx = state_value["conversation_contexts"][
232
+ state_value["conversation_id"]]
233
+ if ctx.get("history") and ctx["history"][-1].get("role") == "assistant":
234
+ ctx["history"][-1]["metadata"] = ctx["history"][-1].get(
235
+ "metadata", {})
236
+ ctx["history"][-1]["metadata"]["footer"] = "Chat completion paused"
237
+ return (gr.update(value=ctx.get("history", [])),
238
+ gr.update(value=state_value), gr.update(visible=True),
239
+ gr.update(visible=False))
240
+
241
+ # ----------------------------------------------------------------- state IO
242
+ @staticmethod
243
+ def save_browser_state(state_value):
244
+ return gr.update(value=dict(
245
+ conversations=state_value["conversations"],
246
+ conversation_contexts=state_value["conversation_contexts"]))
247
+
248
+ @staticmethod
249
+ def load_browser_state(browser_state_value, state_value):
250
+ if not browser_state_value:
251
+ return gr.skip(), gr.skip()
252
+ state_value["conversations"] = browser_state_value.get("conversations", [])
253
+ state_value["conversation_contexts"] = browser_state_value.get("conversation_contexts", {})
254
+ return _conv_choices(state_value), gr.update(value=state_value)
255
+
256
+ css = open("./app.css", "r").read()
257
+
258
+ with gr.Blocks(fill_width=True, title="Demo Chat") as demo:
259
+ state = gr.State({
260
+ "conversation_contexts": {},
261
+ "conversations": [],
262
+ "conversation_id": "",
263
+ })
264
+
265
+ with gr.Row(elem_id="main-row"):
266
+ # ============== Sidebar ============================================
267
+ with gr.Column(scale=0, min_width=260, elem_id="sidebar"):
268
+ new_chat_btn = gr.Button(
269
+ value="New Conversation",
270
+ variant="primary",
271
+ )
272
+ conv_choice = gr.Radio(
273
+ choices=[],
274
+ label="Conversations",
275
+ interactive=True,
276
+ elem_id="conversations-radio",
277
+ )
278
+ delete_btn = gr.Button(
279
+ value="Delete Selected",
280
+ variant="stop",
281
+ )
282
+
283
+ # ============== Main chat column ==================================
284
+ with gr.Column(scale=1, elem_id="chat-column"):
285
+ # Action bar
286
+ with gr.Row():
287
+ clear_btn = gr.Button(
288
+ value="Clear History",
289
+ scale=0,
290
+ min_width=120,
291
+ visible=False,
292
+ )
293
+
294
+ # Chatbot
295
+ chatbot = gr.Chatbot(
296
+ elem_id="chatbot",
297
+ show_label=False,
298
+ buttons=None,
299
+ layout="panel"
300
+ )
301
+ # Input area (msg must be defined here so the input lives
302
+ # at the bottom of the column)
303
+ with gr.Row():
304
+ msg = gr.Textbox(
305
+ placeholder="Type a message and press Enter...",
306
+ show_label=False,
307
+ scale=4,
308
+ container=False,
309
+ )
310
+ send_btn = gr.Button(
311
+ "Send",
312
+ variant="primary",
313
+ scale=0,
314
+ min_width=80,
315
+ )
316
+ stop_btn = gr.Button(
317
+ "Stop",
318
+ variant="stop",
319
+ scale=0,
320
+ min_width=80,
321
+ visible=False,
322
+ )
323
+
324
+ # ============== Event wiring ===========================================
325
+
326
+ # New chat
327
+ new_chat_btn.click(
328
+ fn=GradioEvents.new_chat,
329
+ inputs=[state],
330
+ outputs=[conv_choice, chatbot, state],
331
+ )
332
+
333
+ # Select conversation
334
+ conv_choice.change(
335
+ fn=GradioEvents.select_conversation,
336
+ inputs=[conv_choice, state],
337
+ outputs=[chatbot, state],
338
+ )
339
+
340
+ # Delete conversation
341
+ delete_btn.click(
342
+ fn=GradioEvents.delete_selected_conversation,
343
+ inputs=[conv_choice, state],
344
+ outputs=[conv_choice, chatbot, state],
345
+ )
346
+
347
+ # Clear history of current conversation
348
+ clear_btn.click(
349
+ fn=GradioEvents.clear_history,
350
+ inputs=[state],
351
+ outputs=[chatbot, state],
352
+ )
353
+
354
+ # Send message
355
+ submit_event = send_btn.click(
356
+ fn=GradioEvents.stream_response,
357
+ inputs=[msg, state],
358
+ outputs=[msg, chatbot, state, conv_choice, send_btn, stop_btn],
359
+ )
360
+ msg.submit(
361
+ fn=GradioEvents.stream_response,
362
+ inputs=[msg, state],
363
+ outputs=[msg, chatbot, state, conv_choice, send_btn, stop_btn],
364
+ )
365
+
366
+ # Stop button cancels the streaming
367
+ stop_btn.click(
368
+ fn=GradioEvents.cancel_stream,
369
+ inputs=[state],
370
+ outputs=[chatbot, state, send_btn, stop_btn],
371
+ cancels=[submit_event],
372
+ )
373
+
374
+ browser_state = gr.BrowserState(
375
+ {
376
+ "conversation_contexts": {},
377
+ "conversations": [],
378
+ },
379
+ storage_key="chat_app_state",
380
+ secret="devmode"
381
+ )
382
+ state.change(
383
+ fn=GradioEvents.save_browser_state,
384
+ inputs=[state],
385
+ outputs=[browser_state],
386
+ )
387
+ demo.load(
388
+ fn=GradioEvents.load_browser_state,
389
+ inputs=[browser_state, state],
390
+ outputs=[conv_choice, state],
391
+ )
392
+
393
+ theme = gr.themes.Base(radius_size="none")
394
+
395
+ if __name__ == "__main__":
396
+ demo.queue(default_concurrency_limit=100, max_size=100).launch(ssr_mode=False, max_threads=100, css=css, theme=theme)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio==6.8.0
2
+ openai==2.41.0
3
+ python-dotenv==1.2.2