Skip to content

Commit 3b93501

Browse files
committed
fix query router and formatters
1 parent 9c4e0c0 commit 3b93501

3 files changed

Lines changed: 72 additions & 11 deletions

File tree

packages/ai_service/src/router/query_router.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@
2626
# not per-customer live data, even when a customer name appears in the question.
2727
_PAYMENT_TERMS_STATIC = re.compile(r'付款條件|付款天數|付款期限|payment\s*term', re.IGNORECASE)
2828

29+
# "STM32F103C8 is out of stock. What are the alternatives?" is a procurement
30+
# knowledge question (answered from static knowledge cards), not a live inventory
31+
# lookup — even though "stock" triggers the bare inventory keyword.
32+
_OUT_OF_STOCK_ALT_STATIC = re.compile(
33+
r'out\s+of\s+stock.{0,80}(?:alternative|substitut|replace|instead)'
34+
r'|(?:alternative|substitut|replace).{0,80}out\s+of\s+stock',
35+
re.IGNORECASE,
36+
)
37+
2938

3039
def route(question: str) -> ParsedQuery:
3140
"""Return the routing decision for a user question."""
@@ -41,6 +50,11 @@ def route(question: str) -> ParsedQuery:
4150
and _PAYMENT_TERMS_STATIC.search(question)):
4251
return ParsedQuery(route='static')
4352

53+
# De-escalate: "X is out of stock, what are the alternatives?" is a static
54+
# procurement knowledge question, not a live inventory lookup.
55+
if parsed.route == 'dynamic' and _OUT_OF_STOCK_ALT_STATIC.search(question):
56+
return ParsedQuery(route='static')
57+
4458
# Upgrade: customer detail questions still need a search term before using live lookup.
4559
if parsed.tool == 'customer' and not parsed.search_term:
4660
return ParsedQuery(route='static')

packages/ai_service/src/tools/formatters.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ def format_inventory_answer(product: dict, inventory: dict) -> str:
2626
]
2727

2828
if available <= critical_stock:
29-
lines.append(f"🔴 Critical: Available ({available}) is below critical level ({critical_stock}). Immediate action required.")
29+
lines.append(f"[CRITICAL] Available ({available}) is below critical level ({critical_stock}). Immediate action required.")
3030
elif available <= alert_stock:
31-
lines.append(f"🟠 Alert: Available ({available}) is below alert level ({alert_stock}). Please source urgently.")
31+
lines.append(f"[ALERT] Available ({available}) is below alert level ({alert_stock}). Please source urgently.")
3232
elif available <= min_stock:
33-
lines.append(f"🟡 Warning: Available ({available}) is below minimum level ({min_stock}). Consider replenishing.")
33+
lines.append(f"[WARNING] Available ({available}) is below minimum level ({min_stock}). Consider replenishing.")
3434
else:
35-
lines.append(f" Stock sufficient (available {available} units, above minimum {min_stock} units).")
35+
lines.append(f"[OK] Stock sufficient (available {available} units, above minimum {min_stock} units).")
3636

3737
return "\n".join(lines)
3838

packages/frontend/lib/features/ai/chat_screen.dart

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,13 @@ class _MessageTile extends StatelessWidget {
151151
child: Column(
152152
crossAxisAlignment: CrossAxisAlignment.start,
153153
children: [
154-
Text(
155-
message.content.isEmpty && message.isStreaming
156-
? '▌'
157-
: message.content,
158-
style: TextStyle(
159-
color: isUser ? cs.onPrimaryContainer : cs.onSurface,
154+
if (message.content.isEmpty && message.isStreaming)
155+
Text('▌', style: TextStyle(color: cs.onSurface))
156+
else
157+
_StyledMessageContent(
158+
content: message.content,
159+
baseColor: isUser ? cs.onPrimaryContainer : cs.onSurface,
160160
),
161-
),
162161
if (message.isStreaming && message.content.isNotEmpty)
163162
const Padding(
164163
padding: EdgeInsets.only(top: 4),
@@ -185,6 +184,54 @@ class _MessageTile extends StatelessWidget {
185184
}
186185
}
187186

187+
// 將 [CRITICAL]/[ALERT]/[WARNING]/[OK] 轉為 icon + 文字行
188+
class _StyledMessageContent extends StatelessWidget {
189+
final String content;
190+
final Color baseColor;
191+
192+
const _StyledMessageContent({required this.content, required this.baseColor});
193+
194+
static const _kTags = {
195+
'[CRITICAL]': (Icons.error, Colors.red),
196+
'[ALERT]': (Icons.warning_rounded, Colors.orange),
197+
'[WARNING]': (Icons.warning_amber_outlined, Colors.amber),
198+
'[OK]': (Icons.check_circle_outline, Colors.green),
199+
};
200+
201+
@override
202+
Widget build(BuildContext context) {
203+
final lines = content.split('\n');
204+
return Column(
205+
crossAxisAlignment: CrossAxisAlignment.start,
206+
children: lines.map((line) {
207+
for (final entry in _kTags.entries) {
208+
if (line.startsWith(entry.key)) {
209+
final (icon, color) = entry.value;
210+
final rest = line.substring(entry.key.length).trimLeft();
211+
return Padding(
212+
padding: const EdgeInsets.only(top: 4),
213+
child: Row(
214+
crossAxisAlignment: CrossAxisAlignment.start,
215+
children: [
216+
Icon(icon, size: 16, color: color),
217+
const SizedBox(width: 4),
218+
Expanded(
219+
child: Text(
220+
rest,
221+
style: TextStyle(color: baseColor, fontWeight: FontWeight.w600),
222+
),
223+
),
224+
],
225+
),
226+
);
227+
}
228+
}
229+
return Text(line, style: TextStyle(color: baseColor));
230+
}).toList(),
231+
);
232+
}
233+
}
234+
188235
// ── 快捷測試題按鈕 ────────────────────────────────────────
189236

190237
enum _ChipGroup { dynamic_, static_, blocked }

0 commit comments

Comments
 (0)