Skip to content

Commit 90c7cda

Browse files
authored
[Doc] Add Signal-Decision Architecture blog to README news (#783)
* feat(tools): add dissatisfaction detector and explainer models to playground Add two new dialogue-based models to the HuggingFace Spaces playground: - 😤 Dissatisfaction Detector (dissat-detector): Binary classifier that detects user satisfaction (SAT) or dissatisfaction (DISSAT) in conversational AI interactions. - 🔍 Dissatisfaction Explainer (dissat-explainer): Stage 2 classifier that explains dissatisfaction reasons as NEED_CLARIFICATION, WRONG_ANSWER, or WANT_DIFFERENT. Features: - New dialogue input type with separate fields for query, response, and follow-up messages - Special input format: [USER QUERY], [SYSTEM RESPONSE], [USER FOLLOWUP] - Demo examples for each model Signed-off-by: bitliu <bitliu@tencent.com> * docs: add Signal-Decision Architecture blog to README news Add the latest blog post about Signal-Decision Driven Architecture to the Latest News section in README.md. Blog: https://blog.vllm.ai/2025/11/19/signal-decision.html Signed-off-by: bitliu <bitliu@tencent.com> --------- Signed-off-by: bitliu <bitliu@tencent.com>
1 parent 69ee0b3 commit 90c7cda

File tree

2 files changed

+108
-9
lines changed

2 files changed

+108
-9
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
*Latest News* 🔥
1919

20+
- [2025/11/19] We released the [Signal-Decision Driven Architecture: Reshaping Semantic Routing at Scale](https://blog.vllm.ai/2025/11/19/signal-decision.html) 🧠
2021
- [2025/11/03] **Our paper** [Category-Aware Semantic Caching for Heterogeneous LLM Workloads](https://arxiv.org/abs/2510.26835) published 📝
2122
- [2025/10/26] We reached 2000 stars on GitHub! 🔥
2223
- [2025/10/21] We announced the [2025 Q4 Roadmap: Journey to Iris](https://vllm-semantic-router.com/blog/q4-roadmap-iris) 📅.

tools/hf-playground/app.py

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,32 @@
7878
"labels": None,
7979
"demo": "John Smith works at Microsoft in Seattle, his email is john.smith@microsoft.com",
8080
},
81+
"😤 Dissatisfaction Detector": {
82+
"id": "llm-semantic-router/dissat-detector",
83+
"description": "Detects user dissatisfaction in conversational AI interactions. Classifies user follow-up messages as satisfied (SAT) or dissatisfied (DISSAT).",
84+
"type": "dialogue",
85+
"labels": {0: ("SAT", "🟢"), 1: ("DISSAT", "🔴")},
86+
"demo": {
87+
"query": "Find a restaurant nearby",
88+
"response": "I found Italian Kitchen for you.",
89+
"followup": "Show me other options",
90+
},
91+
},
92+
"🔍 Dissatisfaction Explainer": {
93+
"id": "llm-semantic-router/dissat-explainer",
94+
"description": "Explains why a user is dissatisfied. Stage 2 of hierarchical dissatisfaction detection - classifies into NEED_CLARIFICATION, WRONG_ANSWER, or WANT_DIFFERENT.",
95+
"type": "dialogue",
96+
"labels": {
97+
0: ("NEED_CLARIFICATION", "❓"),
98+
1: ("WRONG_ANSWER", "❌"),
99+
2: ("WANT_DIFFERENT", "🔄"),
100+
},
101+
"demo": {
102+
"query": "Book a table for 2",
103+
"response": "Table for 3 confirmed",
104+
"followup": "No, I said 2 people not 3",
105+
},
106+
},
81107
}
82108

83109

@@ -109,6 +135,26 @@ def classify_sequence(text: str, model_id: str, labels: dict) -> tuple:
109135
return label_name, emoji, confidence, all_scores
110136

111137

138+
def classify_dialogue(
139+
query: str, response: str, followup: str, model_id: str, labels: dict
140+
) -> tuple:
141+
"""Classify dialogue using sequence classification model with special format."""
142+
tokenizer, model = load_model(model_id, "sequence")
143+
# Format input as per model requirements
144+
text = f"[USER QUERY] {query}\n[SYSTEM RESPONSE] {response}\n[USER FOLLOWUP] {followup}"
145+
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
146+
with torch.no_grad():
147+
outputs = model(**inputs)
148+
probs = torch.softmax(outputs.logits, dim=-1)[0]
149+
pred_class = torch.argmax(probs).item()
150+
label_name, emoji = labels[pred_class]
151+
confidence = probs[pred_class].item()
152+
all_scores = {
153+
f"{labels[i][1]} {labels[i][0]}": float(probs[i]) for i in range(len(labels))
154+
}
155+
return label_name, emoji, confidence, all_scores
156+
157+
112158
def classify_tokens(text: str, model_id: str) -> list:
113159
"""Token-level NER classification."""
114160
tokenizer, model = load_model(model_id, "token")
@@ -211,18 +257,70 @@ def main():
211257

212258
# Main content
213259
st.subheader("📝 Input")
214-
text_input = st.text_area(
215-
"Enter text to analyze:",
216-
value=model_config["demo"],
217-
height=120,
218-
placeholder="Type your text here...",
219-
)
260+
261+
# Different input UI based on model type
262+
if model_config["type"] == "dialogue":
263+
# Dialogue models need query, response, and followup
264+
demo = model_config["demo"]
265+
query_input = st.text_input(
266+
"🗣️ User Query:",
267+
value=demo["query"],
268+
placeholder="Enter the original user query...",
269+
)
270+
response_input = st.text_input(
271+
"🤖 System Response:",
272+
value=demo["response"],
273+
placeholder="Enter the system's response...",
274+
)
275+
followup_input = st.text_input(
276+
"💬 User Follow-up:",
277+
value=demo["followup"],
278+
placeholder="Enter the user's follow-up message...",
279+
)
280+
text_input = None # Not used for dialogue models
281+
else:
282+
# Standard text input for other models
283+
text_input = st.text_area(
284+
"Enter text to analyze:",
285+
value=model_config["demo"],
286+
height=120,
287+
placeholder="Type your text here...",
288+
)
289+
query_input = response_input = followup_input = None
220290

221291
st.markdown("---")
222292

223293
# Analyze button
224294
if st.button("🔍 Analyze", type="primary", use_container_width=True):
225-
if not text_input.strip():
295+
if model_config["type"] == "dialogue":
296+
if (
297+
not query_input.strip()
298+
or not response_input.strip()
299+
or not followup_input.strip()
300+
):
301+
st.warning("Please fill in all dialogue fields.")
302+
else:
303+
with st.spinner("Analyzing..."):
304+
label, emoji, conf, scores = classify_dialogue(
305+
query_input,
306+
response_input,
307+
followup_input,
308+
model_config["id"],
309+
model_config["labels"],
310+
)
311+
st.session_state.result = {
312+
"type": "dialogue",
313+
"label": label,
314+
"emoji": emoji,
315+
"confidence": conf,
316+
"scores": scores,
317+
"input": {
318+
"query": query_input,
319+
"response": response_input,
320+
"followup": followup_input,
321+
},
322+
}
323+
elif not text_input.strip():
226324
st.warning("Please enter some text to analyze.")
227325
else:
228326
with st.spinner("Analyzing..."):
@@ -250,7 +348,7 @@ def main():
250348
st.markdown("---")
251349
st.subheader("📊 Results")
252350
result = st.session_state.result
253-
if result["type"] == "sequence":
351+
if result["type"] in ("sequence", "dialogue"):
254352
col1, col2 = st.columns([1, 1])
255353
with col1:
256354
st.success(f"{result['emoji']} **{result['label']}**")
@@ -262,7 +360,7 @@ def main():
262360
)
263361
for k, v in sorted_scores.items():
264362
st.progress(v, text=f"{k}: {v:.1%}")
265-
else:
363+
elif result["type"] == "token":
266364
entities = result["entities"]
267365
if entities:
268366
st.success(f"Found {len(entities)} PII entity(s)")

0 commit comments

Comments
 (0)