fix(nordagpt): structural anti-hallucination — validate ALL company links against DB
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

Prompt rules don't work — AI ignores them and invents company names.
Added _validate_company_references() post-processor that:
- Loads all valid company slugs from DB
- Scans every /firma/ link in AI response
- REMOVES links to companies that don't exist
- Cleans up empty list items left by removals
- Applied to BOTH send_message() and send_message_stream()

This is the ONLY reliable way to prevent hallucinated companies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-28 06:25:20 +01:00
parent d7a8cbe459
commit c167794bb6

View File

@ -134,6 +134,61 @@ class NordaBizChatEngine:
self.model_name = self.gemini_service.model_name
self.model = None
@staticmethod
def _get_valid_company_slugs() -> set:
"""Load all valid company slugs from DB. Cached per-request."""
db = SessionLocal()
try:
from database import Company
rows = db.query(Company.slug, Company.name).filter(Company.status == 'active').all()
return {r.slug: r.name for r in rows if r.slug}
finally:
db.close()
@staticmethod
def _validate_company_references(text: str) -> str:
"""
Post-process AI response: remove links to companies that don't exist in DB.
This is the ONLY reliable way to prevent hallucinated company names.
"""
import re
valid_companies = NordaBizChatEngine._get_valid_company_slugs()
valid_slugs = set(valid_companies.keys())
valid_names_lower = {name.lower(): name for name in valid_companies.values()}
# 1. Validate markdown links to /firma/slug — remove if slug doesn't exist
def replace_link(match):
link_text = match.group(1)
slug = match.group(2)
if slug in valid_slugs:
return match.group(0) # Keep valid link
else:
logger.warning(f"NordaGPT hallucination blocked: removed link to non-existent company slug '{slug}' (text: '{link_text}')")
return '' # Remove entire link
text = re.sub(r'\[([^\]]+)\]\(/firma/([a-z0-9-]+)\)', replace_link, text)
# 2. Validate pill-style links that the frontend renders
def replace_pill_link(match):
full_match = match.group(0)
slug = match.group(1)
if slug in valid_slugs:
return full_match
else:
logger.warning(f"NordaGPT hallucination blocked: removed pill link to '{slug}'")
return ''
text = re.sub(r'<a[^>]*href=["\']/firma/([a-z0-9-]+)["\'][^>]*>.*?</a>', replace_pill_link, text)
# 3. Clean up empty list items and double spaces left by removals
text = re.sub(r'\n\s*\*\s*\n', '\n', text) # empty bullet points
text = re.sub(r'\n\s*-\s*\n', '\n', text) # empty list items
text = re.sub(r' +', ' ', text) # double spaces
text = re.sub(r'\n{3,}', '\n\n', text) # triple+ newlines
return text.strip()
def start_conversation(
self,
user_id: int,
@ -294,6 +349,9 @@ class NordaBizChatEngine:
user_context=user_context
)
# CRITICAL: Validate all company references — remove hallucinated firms
response = self._validate_company_references(response)
# Calculate metrics for per-message tracking in AIChatMessage table
latency_ms = int((time.time() - start_time) * 1000)
if self.gemini_service:
@ -1644,6 +1702,9 @@ W dyskusji [Artur Wiertel](link) pytał o moderację. Pełna treść: [moje uwag
# Post-process links in full response
full_response_text = self._postprocess_links(full_response_text, context)
# CRITICAL: Validate all company references — remove hallucinated firms
full_response_text = self._validate_company_references(full_response_text)
# Calculate metrics
latency_ms = int((time.time() - start_time) * 1000)
input_tokens = len(full_prompt) // 4