hadadrjt commited on
Commit
e0bcfd2
·
1 Parent(s): 08ed4d4

ai: Render reasoning tag.

Browse files
Files changed (1) hide show
  1. 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 async handler for user input submission.
18
- Supports text + file uploads (multi-modal input).
19
  Extracts file content and appends to user input.
20
- Streams AI responses back to UI, updating chat history live.
21
- Allows stopping response generation gracefully.
 
22
  """
 
23
  ensure_stop_event(sess)
24
  sess.stop_event.clear()
25
  sess.cancel_token["cancelled"] = False
26
- # Extract text and files from multimodal input
 
27
  msg_input = {"text": multi.get("text", "").strip(), "files": multi.get("files", [])}
28
- # If no input, reset UI state and return
 
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
- # Initialize input with extracted file contents
 
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
- # Append user text input if any
 
39
  if msg_input["text"]:
40
  inp += msg_input["text"]
41
- # Append user input to chat history with placeholder response
 
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
- # Background async task to fetch streamed AI responses
 
46
  async def background():
47
- reasoning = ""
48
- responses = ""
49
- content_started = False
50
- ignore_reasoning = False
 
 
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
- if typ == "reasoning":
55
- if ignore_reasoning:
56
- continue
57
- reasoning += chunk
58
- await queue.put(("reasoning", reasoning))
59
- elif typ == "content":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  if not content_started:
 
61
  content_started = True
62
- ignore_reasoning = True
63
- responses = chunk
64
- await queue.put(("reasoning", "")) # Clear reasoning on content start
65
- await queue.put(("replace", responses))
 
66
  else:
67
- responses += chunk
68
- await queue.put(("append", responses))
 
 
 
 
69
  await queue.put(None)
70
- return responses
 
 
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 requested stop, cancel background task and update UI
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
- # Update last message content in history with streamed text
97
- history[-1][1] = text
 
 
 
 
 
 
 
 
 
 
 
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):