fix(nordagpt): empty matcher fallback, no-results prompt, hide costs for users, streaming timeout
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

1. When company_matcher returns [], keep full company list as fallback instead of leaving AI with zero data
2. Add explicit "no results" instruction in prompt to prevent hallucinated company names
3. Hide cost badge chip from non-admin users (IS_ADMIN gate)
4. Add 60s AbortController timeout on streaming fetch to prevent hung connections

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-28 13:08:44 +01:00
parent 50d31c1b84
commit 8ee998b42f
2 changed files with 25 additions and 7 deletions

View File

@ -461,8 +461,12 @@ class NordaBizChatEngine:
if COMPANY_MATCHER_AVAILABLE:
try:
matched = match_companies(user_message, user_context=user_context, max_results=15)
context['matched_companies'] = matched
context['all_companies'] = [] # Clear full list — use matched only
if matched: # Only use matcher results if non-empty
context['matched_companies'] = matched
context['all_companies'] = [] # Clear full list — use matched only
else:
logger.info("Company matcher returned 0 results — keeping full company list as fallback")
# Don't clear all_companies — AI will use the full list
logger.info(f"Company matcher found {len(matched)} companies for query")
except Exception as e:
logger.warning(f"Company matcher failed: {e}, using full company list")
@ -1517,6 +1521,10 @@ W dyskusji [Artur Wiertel](link) pytał o moderację. Pełna treść: [moje uwag
system_prompt += "\n\n🏢 SZCZEGÓŁY FIRM:\n"
system_prompt += json.dumps(context['all_companies'], ensure_ascii=False, indent=None)
system_prompt += "\n"
else:
system_prompt += "\n\n⚠️ BRAK DOPASOWANYCH FIRM: Wyszukiwanie nie znalazło firm bezpośrednio pasujących do tego zapytania. "
system_prompt += "Odpowiedz na pytanie ogólnie, bez wymieniania konkretnych nazw firm. "
system_prompt += "NIE WYMYŚLAJ nazw firm. Jeśli pytanie dotyczy firm, powiedz: 'W bazie Izby nie znalazłem firmy o takim profilu.'\n"
# Add recommendations (peer endorsements)
if context.get('recommendations'):
@ -1829,8 +1837,12 @@ W dyskusji [Artur Wiertel](link) pytał o moderację. Pełna treść: [moje uwag
if COMPANY_MATCHER_AVAILABLE:
try:
matched = match_companies(user_message, user_context=user_context, max_results=15)
context['matched_companies'] = matched
context['all_companies'] = [] # Clear full list — use matched only
if matched: # Only use matcher results if non-empty
context['matched_companies'] = matched
context['all_companies'] = [] # Clear full list — use matched only
else:
logger.info("Company matcher (stream) returned 0 results — keeping full company list as fallback")
# Don't clear all_companies — AI will use the full list
logger.info(f"Company matcher (stream) found {len(matched)} companies for query")
except Exception as e:
logger.warning(f"Company matcher failed: {e}, using full company list")

View File

@ -1930,6 +1930,7 @@ let currentConversationId = null;
let conversations = [];
let currentModel = 'flash'; // Default model (Gemini 3 Flash - thinking mode, 10K RPD)
let monthlyUsageCost = 0; // Koszt miesięczny użytkownika
const IS_ADMIN = {{ 'true' if current_user.is_admin or current_user.can_access_admin_panel() else 'false' }};
// ============================================
// Model Selection Toggle Functions
@ -2525,11 +2526,14 @@ async function sendMessage() {
// Try streaming endpoint
let streamingSucceeded = false;
const abortController = new AbortController();
const streamTimeout = setTimeout(() => abortController.abort(), 60000); // 60s timeout
try {
const streamResp = await fetch(`/api/chat/${currentConversationId}/message/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ message: message, model: currentModel })
body: JSON.stringify({ message: message, model: currentModel }),
signal: abortController.signal
});
if (!streamResp.ok) throw new Error(`HTTP ${streamResp.status}`);
@ -2596,7 +2600,7 @@ async function sendMessage() {
let badgeHTML = `<span class="badge-chip model">${modelLabel}</span>`;
if (cIcon && tLabel) badgeHTML += `<span class="badge-chip thinking">${cIcon} ${tLabel}</span>`;
badgeHTML += `<span class="badge-chip time">⏱ ${latencySec}s</span>`;
badgeHTML += `<span class="badge-chip cost">💰 ${costStr}</span>`;
if (IS_ADMIN) badgeHTML += `<span class="badge-chip cost">💰 ${costStr}</span>`;
infoBadge.innerHTML = badgeHTML;
streamContent.appendChild(infoBadge);
@ -2623,7 +2627,9 @@ async function sendMessage() {
}
}
}
clearTimeout(streamTimeout);
} catch (streamError) {
clearTimeout(streamTimeout);
console.warn('Streaming failed, falling back to non-streaming:', streamError);
// Remove the partial streaming bubble
streamMsgDiv.remove();
@ -2718,7 +2724,7 @@ function addMessage(role, content, animate = true, techInfo = null) {
// Build badge content with chips
let badgeHTML = `<span class="badge-chip model">${modelLabel}</span>`;
badgeHTML += `<span class="badge-chip time">⏱ ${latencySec}s</span>`;
badgeHTML += `<span class="badge-chip cost">💰 ${costStr}</span>`;
if (IS_ADMIN) badgeHTML += `<span class="badge-chip cost">💰 ${costStr}</span>`;
infoBadge.innerHTML = badgeHTML;
contentDiv.appendChild(infoBadge);