Spaces:
Running
Running
ai: Render reasoning tag.
Browse files- src/main/gradio.py +106 -31
src/main/gradio.py
CHANGED
@@ -14,94 +14,169 @@ from src.cores.client import chat_with_model_async
|
|
14 |
|
15 |
async def respond_async(multi, history, model_display, sess, custom_prompt, deep_search):
|
16 |
"""
|
17 |
-
Main
|
18 |
-
Supports text
|
19 |
Extracts file content and appends to user input.
|
20 |
-
Streams AI responses back to UI, updating chat history live.
|
21 |
-
|
|
|
22 |
"""
|
|
|
23 |
ensure_stop_event(sess)
|
24 |
sess.stop_event.clear()
|
25 |
sess.cancel_token["cancelled"] = False
|
26 |
-
|
|
|
27 |
msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
|
28 |
-
|
|
|
29 |
if not msg_input["text"] and not msg_input["files"]:
|
30 |
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
31 |
return
|
32 |
-
|
|
|
33 |
inp = ""
|
34 |
for f in msg_input["files"]:
|
35 |
-
# Support dict or direct file path
|
36 |
fp = f.get("data", f.get("name", "")) if isinstance(f, dict) else f
|
|
|
37 |
inp += f"{Path(fp).name}\n\n{extract_file_content(fp)}\n\n"
|
38 |
-
|
|
|
39 |
if msg_input["text"]:
|
40 |
inp += msg_input["text"]
|
41 |
-
|
|
|
42 |
history.append([inp, RESPONSES["RESPONSE_8"]])
|
|
|
43 |
yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
|
|
|
|
|
44 |
queue = asyncio.Queue()
|
45 |
-
|
|
|
46 |
async def background():
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
51 |
async for typ, chunk in chat_with_model_async(history, inp, model_display, sess, custom_prompt, deep_search):
|
|
|
52 |
if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
|
53 |
break
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
if not content_started:
|
|
|
61 |
content_started = True
|
62 |
-
|
63 |
-
|
64 |
-
await queue.put(("reasoning", ""))
|
65 |
-
|
|
|
66 |
else:
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
|
|
69 |
await queue.put(None)
|
70 |
-
return
|
|
|
|
|
71 |
bg_task = asyncio.create_task(background())
|
|
|
72 |
stop_task = asyncio.create_task(sess.stop_event.wait())
|
|
|
73 |
pending_tasks = {bg_task, stop_task}
|
|
|
74 |
try:
|
75 |
while True:
|
|
|
76 |
queue_task = asyncio.create_task(queue.get())
|
77 |
pending_tasks.add(queue_task)
|
|
|
|
|
78 |
done, _ = await asyncio.wait({stop_task, queue_task}, return_when=asyncio.FIRST_COMPLETED)
|
|
|
79 |
for task in done:
|
80 |
pending_tasks.discard(task)
|
|
|
81 |
if task is stop_task:
|
82 |
-
# User
|
83 |
sess.cancel_token["cancelled"] = True
|
84 |
bg_task.cancel()
|
85 |
try:
|
86 |
await bg_task
|
87 |
except asyncio.CancelledError:
|
|
|
88 |
pass
|
|
|
89 |
history[-1][1] = RESPONSES["RESPONSE_1"]
|
|
|
90 |
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
91 |
return
|
|
|
92 |
result = task.result()
|
93 |
if result is None:
|
|
|
94 |
raise StopAsyncIteration
|
|
|
95 |
action, text = result
|
96 |
-
|
97 |
-
history
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
|
|
|
99 |
except StopAsyncIteration:
|
|
|
100 |
pass
|
101 |
finally:
|
|
|
102 |
for task in pending_tasks:
|
103 |
task.cancel()
|
104 |
await asyncio.gather(*pending_tasks, return_exceptions=True)
|
|
|
|
|
105 |
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
106 |
|
107 |
def toggle_deep_search(deep_search_value, history, sess, prompt, model):
|
|
|
14 |
|
15 |
async def respond_async(multi, history, model_display, sess, custom_prompt, deep_search):
|
16 |
"""
|
17 |
+
Main asynchronous handler for user input submission.
|
18 |
+
Supports text and file uploads (multi-modal input).
|
19 |
Extracts file content and appends to user input.
|
20 |
+
Streams AI responses back to the UI, updating chat history live.
|
21 |
+
Separates and streams 'reasoning' (AI thinking) and final content distinctly.
|
22 |
+
Allows graceful stopping of response generation on user request.
|
23 |
"""
|
24 |
+
# Ensure the stop event object exists in the session and clear any previous stop signals
|
25 |
ensure_stop_event(sess)
|
26 |
sess.stop_event.clear()
|
27 |
sess.cancel_token["cancelled"] = False
|
28 |
+
|
29 |
+
# Extract text and files from the multi-modal user input
|
30 |
msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
|
31 |
+
|
32 |
+
# If no text or files provided, reset UI input state and return immediately
|
33 |
if not msg_input["text"] and not msg_input["files"]:
|
34 |
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
35 |
return
|
36 |
+
|
37 |
+
# Initialize input string by extracting content from uploaded files (if any)
|
38 |
inp = ""
|
39 |
for f in msg_input["files"]:
|
40 |
+
# Support both dict format (with 'data' or 'name') and direct file path
|
41 |
fp = f.get("data", f.get("name", "")) if isinstance(f, dict) else f
|
42 |
+
# Append filename and extracted file content, separated by newlines for clarity
|
43 |
inp += f"{Path(fp).name}\n\n{extract_file_content(fp)}\n\n"
|
44 |
+
|
45 |
+
# Append user text input if present
|
46 |
if msg_input["text"]:
|
47 |
inp += msg_input["text"]
|
48 |
+
|
49 |
+
# Append the combined input to chat history with a placeholder response indicating processing
|
50 |
history.append([inp, RESPONSES["RESPONSE_8"]])
|
51 |
+
# Yield updated history and disable input while processing, enable stop button
|
52 |
yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
|
53 |
+
|
54 |
+
# Create an asynchronous queue to communicate between background streaming task and main loop
|
55 |
queue = asyncio.Queue()
|
56 |
+
|
57 |
+
# Define background async task that fetches streamed AI responses and processes reasoning/content separately
|
58 |
async def background():
|
59 |
+
reasoning_buffer = "" # Buffer to accumulate reasoning (AI's "thinking") text
|
60 |
+
response_buffer = "" # Buffer to accumulate final content (AI's answer)
|
61 |
+
inside_reasoning = False # Flag indicating if currently inside reasoning section
|
62 |
+
content_started = False # Flag indicating if final content streaming has started
|
63 |
+
|
64 |
+
# Receive streamed tuples (type, chunk) from the AI model asynchronously
|
65 |
async for typ, chunk in chat_with_model_async(history, inp, model_display, sess, custom_prompt, deep_search):
|
66 |
+
# If user requested stop, break the streaming loop
|
67 |
if sess.stop_event.is_set() or sess.cancel_token["cancelled"]:
|
68 |
break
|
69 |
+
|
70 |
+
# Detect reasoning start tag in the chunk and enter reasoning mode
|
71 |
+
if "<think>" in chunk:
|
72 |
+
inside_reasoning = True
|
73 |
+
# Capture text after <think> tag as start of reasoning
|
74 |
+
reasoning_buffer += chunk.split("<think>", 1)[1]
|
75 |
+
# Send initial reasoning update to the queue for UI to display reasoning in progress
|
76 |
+
await queue.put(("reasoning", reasoning_buffer))
|
77 |
+
continue # Skip further processing of this chunk
|
78 |
+
|
79 |
+
# Detect reasoning end tag in the chunk and exit reasoning mode
|
80 |
+
if "</think>" in chunk:
|
81 |
+
# Append text before </think> tag to reasoning buffer to complete reasoning text
|
82 |
+
reasoning_buffer += chunk.split("</think>", 1)[0]
|
83 |
+
inside_reasoning = False
|
84 |
+
# Send final reasoning text to queue so UI can mark reasoning as done
|
85 |
+
await queue.put(("reasoning", reasoning_buffer))
|
86 |
+
# Clear reasoning buffer for next possible reasoning section
|
87 |
+
reasoning_buffer = ""
|
88 |
+
continue # Skip further processing of this chunk
|
89 |
+
|
90 |
+
if inside_reasoning:
|
91 |
+
# While inside reasoning, accumulate chunk into reasoning buffer
|
92 |
+
reasoning_buffer += chunk
|
93 |
+
# Send incremental reasoning updates to queue to update UI live
|
94 |
+
await queue.put(("reasoning", reasoning_buffer))
|
95 |
+
else:
|
96 |
+
# Outside reasoning, this chunk belongs to final content answer
|
97 |
if not content_started:
|
98 |
+
# On first content chunk, mark content streaming started and reset reasoning display
|
99 |
content_started = True
|
100 |
+
response_buffer = chunk
|
101 |
+
# Clear any reasoning display on UI as final content begins
|
102 |
+
await queue.put(("reasoning", ""))
|
103 |
+
# Send initial content to queue to replace placeholder message
|
104 |
+
await queue.put(("replace", response_buffer))
|
105 |
else:
|
106 |
+
# Append subsequent content chunks to response buffer
|
107 |
+
response_buffer += chunk
|
108 |
+
# Send updated content to queue to append to existing message on UI
|
109 |
+
await queue.put(("append", response_buffer))
|
110 |
+
|
111 |
+
# Indicate end of streaming by sending None to queue
|
112 |
await queue.put(None)
|
113 |
+
return response_buffer
|
114 |
+
|
115 |
+
# Start background streaming task
|
116 |
bg_task = asyncio.create_task(background())
|
117 |
+
# Create a task that waits for user stop event
|
118 |
stop_task = asyncio.create_task(sess.stop_event.wait())
|
119 |
+
# Track all pending async tasks for proper cancellation
|
120 |
pending_tasks = {bg_task, stop_task}
|
121 |
+
|
122 |
try:
|
123 |
while True:
|
124 |
+
# Create a task to get next item from queue
|
125 |
queue_task = asyncio.create_task(queue.get())
|
126 |
pending_tasks.add(queue_task)
|
127 |
+
|
128 |
+
# Wait until either user stops or new data arrives from queue
|
129 |
done, _ = await asyncio.wait({stop_task, queue_task}, return_when=asyncio.FIRST_COMPLETED)
|
130 |
+
|
131 |
for task in done:
|
132 |
pending_tasks.discard(task)
|
133 |
+
|
134 |
if task is stop_task:
|
135 |
+
# User pressed stop button: cancel background streaming task
|
136 |
sess.cancel_token["cancelled"] = True
|
137 |
bg_task.cancel()
|
138 |
try:
|
139 |
await bg_task
|
140 |
except asyncio.CancelledError:
|
141 |
+
# Expected cancellation exception; ignore
|
142 |
pass
|
143 |
+
# Update last chat message with cancellation notice
|
144 |
history[-1][1] = RESPONSES["RESPONSE_1"]
|
145 |
+
# Yield updated history and reset UI input state accordingly
|
146 |
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
147 |
return
|
148 |
+
|
149 |
result = task.result()
|
150 |
if result is None:
|
151 |
+
# None signals that streaming is complete; stop iteration
|
152 |
raise StopAsyncIteration
|
153 |
+
|
154 |
action, text = result
|
155 |
+
|
156 |
+
# Update chat history based on action type from queue
|
157 |
+
if action == "reasoning":
|
158 |
+
# Update last message content with current reasoning text (AI thinking)
|
159 |
+
history[-1][1] = text
|
160 |
+
elif action == "replace":
|
161 |
+
# Replace last message content with initial content chunk (start of answer)
|
162 |
+
history[-1][1] = text
|
163 |
+
elif action == "append":
|
164 |
+
# Append new content chunk to last message content (streaming answer)
|
165 |
+
history[-1][1] = text
|
166 |
+
|
167 |
+
# Yield updated chat history and UI state (disable input, enable stop)
|
168 |
yield history, gr.update(interactive=False, submit_btn=False, stop_btn=True), sess
|
169 |
+
|
170 |
except StopAsyncIteration:
|
171 |
+
# Streaming ended normally; exit loop
|
172 |
pass
|
173 |
finally:
|
174 |
+
# Cancel all pending tasks to clean up properly
|
175 |
for task in pending_tasks:
|
176 |
task.cancel()
|
177 |
await asyncio.gather(*pending_tasks, return_exceptions=True)
|
178 |
+
|
179 |
+
# After streaming completes, reset UI input to allow new user input
|
180 |
yield history, gr.update(value="", interactive=True, submit_btn=True, stop_btn=False), sess
|
181 |
|
182 |
def toggle_deep_search(deep_search_value, history, sess, prompt, model):
|