utkarshshukla2912 commited on
Commit
e49928e
·
1 Parent(s): a9a3f80

added changes

Browse files
Files changed (3) hide show
  1. app.py +46 -3
  2. config.py +39 -1
  3. src/clients.py +105 -12
app.py CHANGED
@@ -10,6 +10,7 @@ Prompts/intros may contain {VARIABLE} tokens — those are surfaced as fill-in f
10
  in the sidebar and substituted into the effective prompt when a session starts.
11
  """
12
 
 
13
  import queue
14
  import re
15
  import threading
@@ -102,6 +103,8 @@ def format_metrics(m: dict) -> str:
102
  if m.get("error"):
103
  return f"⚠️ {m['error']}"
104
  parts = []
 
 
105
  if m.get("prompt_tokens") is not None:
106
  parts.append(f"In: {m['prompt_tokens']}")
107
  if m.get("completion_tokens") is not None:
@@ -113,6 +116,28 @@ def format_metrics(m: dict) -> str:
113
  return "  ·  ".join(parts)
114
 
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  def backend_messages(system_prompt: str, conversation: list) -> list:
117
  """Build the request messages for one backend: system + its own thread."""
118
  msgs = []
@@ -186,7 +211,7 @@ def respond_all(user_msg, state, temperature, max_tokens):
186
  if not user_msg or not user_msg.strip():
187
  chatbots = [render_history(histories[k]) for k in KEYS]
188
  metrics = [gr.update() for _ in KEYS]
189
- yield (state, *chatbots, *metrics, "", None)
190
  return
191
 
192
  user_msg = user_msg.strip()
@@ -199,6 +224,7 @@ def respond_all(user_msg, state, temperature, max_tokens):
199
 
200
  acc = {k: "" for k in KEYS}
201
  mstate = {k: {} for k in KEYS}
 
202
  q: queue.Queue = queue.Queue()
203
 
204
  def worker(key):
@@ -207,6 +233,8 @@ def respond_all(user_msg, state, temperature, max_tokens):
207
  for item in stream_backend(backend, req_msgs[key], temperature, max_tokens):
208
  if isinstance(item, dict) and item.get("__metrics__"):
209
  q.put((key, "metrics", item))
 
 
210
  else:
211
  q.put((key, "delta", item))
212
  finally:
@@ -222,7 +250,8 @@ def respond_all(user_msg, state, temperature, max_tokens):
222
  histories[k][-1]["content"] = acc[k]
223
  chatbots.append(render_history(histories[k]))
224
  metrics = [format_metrics(mstate[k]) for k in KEYS]
225
- return (state, *chatbots, *metrics, "", None)
 
226
 
227
  remaining = set(KEYS)
228
  yield snapshot()
@@ -236,6 +265,8 @@ def respond_all(user_msg, state, temperature, max_tokens):
236
  acc[key] += payload
237
  elif kind == "metrics":
238
  mstate[key] = payload
 
 
239
  elif kind == "done":
240
  remaining.discard(key)
241
  yield snapshot()
@@ -266,6 +297,7 @@ def respond_all(user_msg, state, temperature, max_tokens):
266
  "total_tokens",
267
  "cached_tokens",
268
  "latency_s",
 
269
  )
270
  },
271
  "error": mstate[k].get("error"),
@@ -443,6 +475,17 @@ with gr.Blocks(
443
  with gr.Row():
444
  send_btn = gr.Button("Send", variant="primary")
445
 
 
 
 
 
 
 
 
 
 
 
 
446
  # Pick the best response for this turn and save it to the dataset.
447
  with gr.Group(visible=SHOW_COMPARE):
448
  gr.Markdown("### 💾 Save preferred response")
@@ -458,7 +501,7 @@ with gr.Blocks(
458
 
459
  # Wiring -----------------------------------------------------------------
460
  respond_inputs = [msg, state, temperature, max_tokens]
461
- respond_outputs = [state, *chatbots, *metrics, msg, preferred]
462
 
463
  preset_outputs = [system_prompt, intro, var_names, *var_boxes]
464
 
 
10
  in the sidebar and substituted into the effective prompt when a session starts.
11
  """
12
 
13
+ import json
14
  import queue
15
  import re
16
  import threading
 
103
  if m.get("error"):
104
  return f"⚠️ {m['error']}"
105
  parts = []
106
+ if m.get("tool_called"):
107
+ parts.append(f"📞 tool: `{m['tool_called']}`")
108
  if m.get("prompt_tokens") is not None:
109
  parts.append(f"In: {m['prompt_tokens']}")
110
  if m.get("completion_tokens") is not None:
 
116
  return "  ·  ".join(parts)
117
 
118
 
119
+ def format_debug(acc: dict, mstate: dict, rstate: dict) -> str:
120
+ """Render the exact request + model response(s) for the current turn as JSON.
121
+
122
+ Shows, per backend: the exact request body sent to the model, the
123
+ live-streamed text, and — once the turn completes — the raw assistant
124
+ message the model produced (`content` + any `tool_calls` with verbatim
125
+ arguments), so both sides of the exchange are visible exactly as sent.
126
+ """
127
+ payload = {}
128
+ for k in KEYS:
129
+ m = mstate.get(k) or {}
130
+ payload[ANON[k]] = {
131
+ "label": BY_KEY[k]["label"],
132
+ "request": rstate.get(k),
133
+ "streamed_text": acc.get(k, ""),
134
+ "raw_response": m.get("raw_response"),
135
+ "tool_called": m.get("tool_called"),
136
+ "error": m.get("error"),
137
+ }
138
+ return json.dumps(payload, ensure_ascii=False, indent=2)
139
+
140
+
141
  def backend_messages(system_prompt: str, conversation: list) -> list:
142
  """Build the request messages for one backend: system + its own thread."""
143
  msgs = []
 
211
  if not user_msg or not user_msg.strip():
212
  chatbots = [render_history(histories[k]) for k in KEYS]
213
  metrics = [gr.update() for _ in KEYS]
214
+ yield (state, *chatbots, *metrics, "", None, gr.update())
215
  return
216
 
217
  user_msg = user_msg.strip()
 
224
 
225
  acc = {k: "" for k in KEYS}
226
  mstate = {k: {} for k in KEYS}
227
+ rstate = {k: None for k in KEYS} # exact request payload sent per backend
228
  q: queue.Queue = queue.Queue()
229
 
230
  def worker(key):
 
233
  for item in stream_backend(backend, req_msgs[key], temperature, max_tokens):
234
  if isinstance(item, dict) and item.get("__metrics__"):
235
  q.put((key, "metrics", item))
236
+ elif isinstance(item, dict) and item.get("__request__"):
237
+ q.put((key, "request", item.get("payload")))
238
  else:
239
  q.put((key, "delta", item))
240
  finally:
 
250
  histories[k][-1]["content"] = acc[k]
251
  chatbots.append(render_history(histories[k]))
252
  metrics = [format_metrics(mstate[k]) for k in KEYS]
253
+ debug = format_debug(acc, mstate, rstate)
254
+ return (state, *chatbots, *metrics, "", None, debug)
255
 
256
  remaining = set(KEYS)
257
  yield snapshot()
 
265
  acc[key] += payload
266
  elif kind == "metrics":
267
  mstate[key] = payload
268
+ elif kind == "request":
269
+ rstate[key] = payload
270
  elif kind == "done":
271
  remaining.discard(key)
272
  yield snapshot()
 
297
  "total_tokens",
298
  "cached_tokens",
299
  "latency_s",
300
+ "tool_called",
301
  )
302
  },
303
  "error": mstate[k].get("error"),
 
475
  with gr.Row():
476
  send_btn = gr.Button("Send", variant="primary")
477
 
478
+ # Debug: the exact request body sent and the raw response the model
479
+ # produced this turn (text + any tool calls with verbatim
480
+ # arguments), per backend. Collapsed by default.
481
+ with gr.Accordion("🐞 Debug — exact request & response", open=False):
482
+ debug_box = gr.Code(
483
+ label="Exact request & raw response (per backend)",
484
+ language="json",
485
+ value="",
486
+ interactive=False,
487
+ )
488
+
489
  # Pick the best response for this turn and save it to the dataset.
490
  with gr.Group(visible=SHOW_COMPARE):
491
  gr.Markdown("### 💾 Save preferred response")
 
501
 
502
  # Wiring -----------------------------------------------------------------
503
  respond_inputs = [msg, state, temperature, max_tokens]
504
+ respond_outputs = [state, *chatbots, *metrics, msg, preferred, debug_box]
505
 
506
  preset_outputs = [system_prompt, intro, var_names, *var_boxes]
507
 
config.py CHANGED
@@ -119,6 +119,42 @@ DEFAULT_MAX_TOKENS = 1000
119
  DEFAULT_STREAM = True
120
 
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  # ---------------------------------------------------------------------------
123
  # Preset system-prompt / intro pairs people can pick to try the playground.
124
  # The first entry is the default loaded on startup. Each preset declares a fixed
@@ -403,7 +439,9 @@ def get_auth():
403
  pairs.append((user, pwd))
404
  password = os.environ.get("APP_PASSWORD", "").strip()
405
  if password:
406
- pairs.append((os.environ.get("APP_USERNAME", "user").strip() or "user", password))
 
 
407
  if not pairs:
408
  # Dev fallback — override via env in any shared deployment.
409
  pairs.append(("user", "slm-demo"))
 
119
  DEFAULT_STREAM = True
120
 
121
 
122
+ # ---------------------------------------------------------------------------
123
+ # Tools exposed to the conversational backend (Qwen, Backend A). The preset
124
+ # system prompts instruct the model to emit `end_call` when the conversation
125
+ # reaches its natural conclusion, placing the goodbye line in `final_message`.
126
+ # The tool is sent in the request body (OpenAI-style `tools` array) so the
127
+ # model can actually call it. Only the custom backend receives these tools.
128
+ # ---------------------------------------------------------------------------
129
+ END_CALL_TOOL = {
130
+ "type": "function",
131
+ "function": {
132
+ "name": "end_call",
133
+ "description": (
134
+ "End the current call when the conversation has reached a natural "
135
+ "conclusion or user says bye or tells to cut the call or speak with "
136
+ "you later as they are busy."
137
+ ),
138
+ "parameters": {
139
+ "type": "object",
140
+ "properties": {
141
+ "final_message": {
142
+ "type": "string",
143
+ "description": (
144
+ "The final message to say to the user before ending the "
145
+ "call. Keep it short and less than 15 words."
146
+ ),
147
+ }
148
+ },
149
+ "required": ["final_message"],
150
+ },
151
+ },
152
+ }
153
+
154
+ # Tools sent to the custom (Qwen) backend on every request.
155
+ TOOLS = [END_CALL_TOOL]
156
+
157
+
158
  # ---------------------------------------------------------------------------
159
  # Preset system-prompt / intro pairs people can pick to try the playground.
160
  # The first entry is the default loaded on startup. Each preset declares a fixed
 
439
  pairs.append((user, pwd))
440
  password = os.environ.get("APP_PASSWORD", "").strip()
441
  if password:
442
+ pairs.append(
443
+ (os.environ.get("APP_USERNAME", "user").strip() or "user", password)
444
+ )
445
  if not pairs:
446
  # Dev fallback — override via env in any shared deployment.
447
  pairs.append(("user", "slm-demo"))
src/clients.py CHANGED
@@ -73,6 +73,13 @@ def _empty_metrics(**overrides) -> dict:
73
  "total_tokens": None,
74
  "cached_tokens": None,
75
  "latency_s": None,
 
 
 
 
 
 
 
76
  "error": None,
77
  }
78
  base.update(overrides)
@@ -125,8 +132,14 @@ def chat_completion(
125
  endpoint_id: str,
126
  temperature: float,
127
  max_tokens: int,
 
128
  ) -> tuple:
129
- """Return (content, usage) from a non-streaming chat completion."""
 
 
 
 
 
130
  payload = {
131
  "messages": messages,
132
  "model": model,
@@ -135,6 +148,8 @@ def chat_completion(
135
  "max_tokens": int(max_tokens),
136
  "stream": False,
137
  }
 
 
138
  headers = {"Content-Type": "application/json", "x-api-key": get_api_key()}
139
  resp = requests.post(config.BASE_URL, headers=headers, json=payload, timeout=120)
140
  if resp.status_code != 200:
@@ -143,20 +158,47 @@ def chat_completion(
143
  choices = data.get("choices") or []
144
  content = ""
145
  if choices:
146
- content = (choices[0].get("message") or {}).get("content") or ""
 
 
 
147
  usage = data.get("usage") or {}
148
  return content, usage
149
 
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  def stream_chat(
152
  messages: list,
153
  model: str,
154
  endpoint_id: str,
155
  temperature: float,
156
  max_tokens: int,
 
157
  ):
158
  """Yield response text deltas, then a final metrics sentinel.
159
 
 
 
 
 
 
160
  Token counts come from a lightweight max_tokens=1 probe fired AFTER the
161
  stream (so it never competes with streaming for GPU); completion tokens are
162
  the streamed-piece count.
@@ -169,11 +211,20 @@ def stream_chat(
169
  "max_tokens": int(max_tokens),
170
  "stream": True,
171
  }
 
 
172
  headers = {"Content-Type": "application/json", "x-api-key": get_api_key()}
173
 
 
 
 
174
  t_start = time.monotonic()
175
  piece_count = 0
176
  ttfb = 0
 
 
 
 
177
  with requests.post(
178
  config.BASE_URL, headers=headers, json=payload, stream=True, timeout=120
179
  ) as resp:
@@ -201,12 +252,35 @@ def stream_chat(
201
  piece = delta.get("content")
202
  if piece:
203
  piece_count += 1
 
204
  yield piece
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  # Probe for token counts now that streaming is done.
207
  try:
208
  _, probe = chat_completion(
209
- messages, model, endpoint_id, temperature, max_tokens=1
210
  )
211
  except Exception:
212
  probe = {}
@@ -215,18 +289,32 @@ def stream_chat(
215
  cached_tokens = details.get("cached_tokens")
216
  total = (prompt_tokens + piece_count) if prompt_tokens is not None else None
217
 
 
 
 
 
 
 
218
  yield _empty_metrics(
219
  prompt_tokens=prompt_tokens,
220
  completion_tokens=piece_count,
221
  total_tokens=total,
222
  cached_tokens=cached_tokens,
223
  latency_s=ttfb,
 
 
224
  )
225
 
226
 
227
  def _stream_custom(backend, messages, temperature, max_tokens):
 
228
  yield from stream_chat(
229
- messages, backend["model"], backend["endpoint_id"], temperature, max_tokens
 
 
 
 
 
230
  )
231
 
232
 
@@ -255,14 +343,17 @@ def _stream_azure(backend, messages, temperature, max_tokens):
255
  t_start = time.monotonic()
256
  usage = None
257
 
258
- stream = client.chat.completions.create(
259
- model=backend["deployment"],
260
- messages=messages,
261
- temperature=float(temperature),
262
- max_completion_tokens=int(max_tokens),
263
- stream=True,
264
- stream_options={"include_usage": True},
265
- )
 
 
 
266
  for chunk in stream:
267
  if getattr(chunk, "usage", None):
268
  usage = chunk.usage
@@ -272,6 +363,7 @@ def _stream_azure(backend, messages, temperature, max_tokens):
272
  delta = choices[0].delta
273
  piece = getattr(delta, "content", None) if delta else None
274
  if piece:
 
275
  yield piece
276
 
277
  prompt_tokens = completion_tokens = total_tokens = cached_tokens = None
@@ -289,6 +381,7 @@ def _stream_azure(backend, messages, temperature, max_tokens):
289
  total_tokens=total_tokens,
290
  cached_tokens=cached_tokens,
291
  latency_s=round(time.monotonic() - t_start, 3),
 
292
  )
293
 
294
 
 
73
  "total_tokens": None,
74
  "cached_tokens": None,
75
  "latency_s": None,
76
+ # Name of the tool the model invoked this turn (e.g. "end_call"), or
77
+ # None when the reply was ordinary text. Lets the UI/logs flag tool use.
78
+ "tool_called": None,
79
+ # The exact assistant message the model produced this turn, as an
80
+ # OpenAI-style {"content", "tool_calls"} dict (raw tool arguments kept
81
+ # verbatim). Powers the debug panel; None on error.
82
+ "raw_response": None,
83
  "error": None,
84
  }
85
  base.update(overrides)
 
132
  endpoint_id: str,
133
  temperature: float,
134
  max_tokens: int,
135
+ tools: list | None = None,
136
  ) -> tuple:
137
+ """Return (content, usage) from a non-streaming chat completion.
138
+
139
+ When `tools` is provided it is sent in the body so the model may call a
140
+ tool. If the model responds with an `end_call` tool call instead of plain
141
+ text, its `final_message` argument is returned as the content.
142
+ """
143
  payload = {
144
  "messages": messages,
145
  "model": model,
 
148
  "max_tokens": int(max_tokens),
149
  "stream": False,
150
  }
151
+ if tools:
152
+ payload["tools"] = tools
153
  headers = {"Content-Type": "application/json", "x-api-key": get_api_key()}
154
  resp = requests.post(config.BASE_URL, headers=headers, json=payload, timeout=120)
155
  if resp.status_code != 200:
 
158
  choices = data.get("choices") or []
159
  content = ""
160
  if choices:
161
+ message = choices[0].get("message") or {}
162
+ content = message.get("content") or ""
163
+ if not content:
164
+ content = _extract_end_call_message(message.get("tool_calls"))
165
  usage = data.get("usage") or {}
166
  return content, usage
167
 
168
 
169
+ def _extract_end_call_message(tool_calls) -> str:
170
+ """Return the `final_message` from an `end_call` tool call, or "".
171
+
172
+ Accepts the non-streaming `tool_calls` list from a chat message. Other tool
173
+ calls (or malformed arguments) yield an empty string.
174
+ """
175
+ for call in tool_calls or []:
176
+ fn = call.get("function") or {}
177
+ if fn.get("name") != "end_call":
178
+ continue
179
+ try:
180
+ args = json.loads(fn.get("arguments") or "{}")
181
+ except json.JSONDecodeError:
182
+ return ""
183
+ return (args.get("final_message") or "").strip()
184
+ return ""
185
+
186
+
187
  def stream_chat(
188
  messages: list,
189
  model: str,
190
  endpoint_id: str,
191
  temperature: float,
192
  max_tokens: int,
193
+ tools: list | None = None,
194
  ):
195
  """Yield response text deltas, then a final metrics sentinel.
196
 
197
+ When `tools` is provided it is sent in the body so the model may call a
198
+ tool. A streamed `end_call` tool call has no text content, so its
199
+ `final_message` argument (assembled from the streamed argument fragments)
200
+ is yielded as the reply once the stream ends.
201
+
202
  Token counts come from a lightweight max_tokens=1 probe fired AFTER the
203
  stream (so it never competes with streaming for GPU); completion tokens are
204
  the streamed-piece count.
 
211
  "max_tokens": int(max_tokens),
212
  "stream": True,
213
  }
214
+ if tools:
215
+ payload["tools"] = tools
216
  headers = {"Content-Type": "application/json", "x-api-key": get_api_key()}
217
 
218
+ # Surface the exact request body (no secret header) for the debug panel.
219
+ yield {"__request__": True, "payload": payload}
220
+
221
  t_start = time.monotonic()
222
  piece_count = 0
223
  ttfb = 0
224
+ tool_args = "" # concatenated `end_call` argument fragments
225
+ tool_name = None # name of the tool the model invoked, if any
226
+ content_buf = "" # raw text the model streamed (before any tool handling)
227
+ saw_end_call = False
228
  with requests.post(
229
  config.BASE_URL, headers=headers, json=payload, stream=True, timeout=120
230
  ) as resp:
 
252
  piece = delta.get("content")
253
  if piece:
254
  piece_count += 1
255
+ content_buf += piece
256
  yield piece
257
+ # An `end_call` tool call streams as `tool_calls` deltas whose
258
+ # `arguments` strings must be concatenated, then parsed at the end.
259
+ for call in delta.get("tool_calls") or []:
260
+ fn = call.get("function") or {}
261
+ if fn.get("name"):
262
+ tool_name = fn["name"]
263
+ if tool_name == "end_call":
264
+ saw_end_call = True
265
+ frag = fn.get("arguments")
266
+ if frag:
267
+ saw_end_call = True
268
+ tool_args += frag
269
+
270
+ # If the model ended the call via the tool, surface the goodbye line.
271
+ if saw_end_call:
272
+ try:
273
+ final_message = (json.loads(tool_args or "{}").get("final_message") or "").strip()
274
+ except json.JSONDecodeError:
275
+ final_message = ""
276
+ if final_message:
277
+ piece_count += 1
278
+ yield final_message
279
 
280
  # Probe for token counts now that streaming is done.
281
  try:
282
  _, probe = chat_completion(
283
+ messages, model, endpoint_id, temperature, max_tokens=1, tools=tools
284
  )
285
  except Exception:
286
  probe = {}
 
289
  cached_tokens = details.get("cached_tokens")
290
  total = (prompt_tokens + piece_count) if prompt_tokens is not None else None
291
 
292
+ raw_response = {"content": content_buf or None}
293
+ if tool_name:
294
+ raw_response["tool_calls"] = [
295
+ {"function": {"name": tool_name, "arguments": tool_args}}
296
+ ]
297
+
298
  yield _empty_metrics(
299
  prompt_tokens=prompt_tokens,
300
  completion_tokens=piece_count,
301
  total_tokens=total,
302
  cached_tokens=cached_tokens,
303
  latency_s=ttfb,
304
+ tool_called=tool_name,
305
+ raw_response=raw_response,
306
  )
307
 
308
 
309
  def _stream_custom(backend, messages, temperature, max_tokens):
310
+ tools = backend.get("tools", config.TOOLS)
311
  yield from stream_chat(
312
+ messages,
313
+ backend["model"],
314
+ backend["endpoint_id"],
315
+ temperature,
316
+ max_tokens,
317
+ tools=tools,
318
  )
319
 
320
 
 
343
  t_start = time.monotonic()
344
  usage = None
345
 
346
+ content_buf = ""
347
+ request_payload = {
348
+ "model": backend["deployment"],
349
+ "messages": messages,
350
+ "temperature": float(temperature),
351
+ "max_completion_tokens": int(max_tokens),
352
+ "stream": True,
353
+ "stream_options": {"include_usage": True},
354
+ }
355
+ yield {"__request__": True, "payload": request_payload}
356
+ stream = client.chat.completions.create(**request_payload)
357
  for chunk in stream:
358
  if getattr(chunk, "usage", None):
359
  usage = chunk.usage
 
363
  delta = choices[0].delta
364
  piece = getattr(delta, "content", None) if delta else None
365
  if piece:
366
+ content_buf += piece
367
  yield piece
368
 
369
  prompt_tokens = completion_tokens = total_tokens = cached_tokens = None
 
381
  total_tokens=total_tokens,
382
  cached_tokens=cached_tokens,
383
  latency_s=round(time.monotonic() - t_start, 3),
384
+ raw_response={"content": content_buf or None},
385
  )
386
 
387