fix(rag): add fallback imports for semantic_router and temporal_intent

Support both relative and absolute imports for running as module or script.
This commit is contained in:
kempersc 2026-01-09 18:26:40 +01:00
parent bd06e4f864
commit c0d31b3905

View file

@ -33,18 +33,32 @@ from dspy import Example, Prediction, History
from dspy.streaming import StatusMessage, StreamListener, StatusMessageProvider
# Semantic routing (Signal-Decision pattern) for fast LLM-free query classification
from .semantic_router import (
QuerySignals,
RouteConfig,
get_signal_extractor,
get_decision_router,
)
try:
from semantic_router import (
QuerySignals,
RouteConfig,
get_signal_extractor,
get_decision_router,
)
except ImportError:
from .semantic_router import (
QuerySignals,
RouteConfig,
get_signal_extractor,
get_decision_router,
)
# Temporal intent extraction for detailed temporal constraint detection
from .temporal_intent import (
TemporalConstraint,
get_temporal_extractor,
)
try:
from temporal_intent import (
TemporalConstraint,
get_temporal_extractor,
)
except ImportError:
from .temporal_intent import (
TemporalConstraint,
get_temporal_extractor,
)
logger = logging.getLogger(__name__)
@ -4578,6 +4592,7 @@ class HeritageRAGPipeline(dspy.Module):
# TEMPLATE-FIRST APPROACH: Try template-based generation before LLM
template_used = False
template_id = None
template_result = None # Keep full template_result for requires_llm() check
if "sparql" in routing.sources:
# 3a. TRY TEMPLATE-BASED SPARQL FIRST (deterministic, validated)
@ -4933,6 +4948,82 @@ class HeritageRAGPipeline(dspy.Module):
"query_type": detected_query_type,
}
# =================================================================
# FACTUAL QUERY FAST-PATH: Skip LLM for table/map/count queries
# =================================================================
# If template matched and doesn't require LLM prose, skip expensive LLM generation
# This provides instant responses for factual queries (lists, counts, maps)
skip_llm_generation = False
factual_answer_text = None
if template_result and hasattr(template_result, 'requires_llm') and not template_result.requires_llm():
skip_llm_generation = True
response_modes = getattr(template_result, 'response_modes', [])
logger.info(f"[Streaming FAST-PATH] Skipping LLM generation, response_modes={response_modes}")
# Generate answer from ui_template (same logic as main.py non-streaming endpoint)
if hasattr(template_result, 'ui_template') and template_result.ui_template:
lang = language if language in template_result.ui_template else "nl"
ui_tmpl = template_result.ui_template.get(lang, template_result.ui_template.get("nl", ""))
# Build context for template rendering
template_context = {
"result_count": len(retrieved_results),
"count": retrieved_results[0].get("count", len(retrieved_results)) if retrieved_results else 0,
**(template_result.slots or {})
}
# Add human-readable labels for institution types and locations
try:
from schema_labels import get_label_resolver
label_resolver = get_label_resolver()
INSTITUTION_TYPE_LABELS_NL = label_resolver.get_all_institution_type_labels("nl")
SUBREGION_LABELS = label_resolver.get_all_subregion_labels("nl")
except ImportError:
logger.warning("[Streaming FAST-PATH] schema_labels not available, using fallback")
INSTITUTION_TYPE_LABELS_NL = {
"M": "musea", "L": "bibliotheken", "A": "archieven", "G": "galerijen",
"O": "overheidsinstellingen", "R": "onderzoekscentra", "C": "bedrijfsarchieven",
"U": "instellingen", "B": "botanische tuinen en dierentuinen",
"E": "onderwijsinstellingen", "S": "heemkundige kringen", "F": "monumenten",
"I": "immaterieel erfgoedgroepen", "X": "gecombineerde instellingen",
"P": "privéverzamelingen", "H": "religieuze erfgoedsites",
"D": "digitale platforms", "N": "erfgoedorganisaties", "T": "culinair erfgoed"
}
SUBREGION_LABELS = {
"NL-DR": "Drenthe", "NL-FR": "Friesland", "NL-GE": "Gelderland",
"NL-GR": "Groningen", "NL-LI": "Limburg", "NL-NB": "Noord-Brabant",
"NL-NH": "Noord-Holland", "NL-OV": "Overijssel", "NL-UT": "Utrecht",
"NL-ZE": "Zeeland", "NL-ZH": "Zuid-Holland", "NL-FL": "Flevoland"
}
# Add institution_type_nl label
if "institution_type" in (template_result.slots or {}):
type_code = template_result.slots["institution_type"]
template_context["institution_type_nl"] = INSTITUTION_TYPE_LABELS_NL.get(type_code, type_code)
# Add human-readable location label
if "location" in (template_result.slots or {}):
loc_code = template_result.slots["location"]
if loc_code in SUBREGION_LABELS:
template_context["location"] = SUBREGION_LABELS[loc_code]
# Render template with simple replacement (avoids Jinja2 dependency)
factual_answer_text = ui_tmpl
for key, value in template_context.items():
factual_answer_text = factual_answer_text.replace("{{ " + key + " }}", str(value))
factual_answer_text = factual_answer_text.replace("{{" + key + "}}", str(value))
elif "count" in response_modes:
# Count query
count_value = retrieved_results[0].get("count", len(retrieved_results)) if retrieved_results else 0
factual_answer_text = f"Aantal: {count_value}"
else:
# List/table query - simple result count
factual_answer_text = f"Gevonden: {len(retrieved_results)} resultaten."
logger.info(f"[Streaming FAST-PATH] Generated factual answer: {factual_answer_text}")
# =================================================================
# ANSWER GENERATION PHASE - Stream tokens using dspy.streamify
# =================================================================
@ -4952,7 +5043,18 @@ class HeritageRAGPipeline(dspy.Module):
current_field = None # Track which DSPy output field we're in
STREAMABLE_FIELDS = {'answer'} # Only stream these fields to frontend
while not streaming_succeeded and retry_count <= max_stream_retries:
# FAST-PATH: If factual query, yield answer immediately and skip LLM
if skip_llm_generation and factual_answer_text:
answer_text = factual_answer_text
confidence = 1.0 # Factual queries from SPARQL have high confidence
citations = ["SPARQL kennisgraaf"]
follow_up = []
streaming_succeeded = True
yield {"type": "token", "content": answer_text}
logger.info(f"[Streaming FAST-PATH] Yielded factual answer, skipping LLM generation")
# STANDARD PATH: Use LLM for prose generation
while not skip_llm_generation and not streaming_succeeded and retry_count <= max_stream_retries:
try:
# Create streamified version of the answer generator
streamified_answer_gen = dspy.streamify(self.answer_gen)
@ -5109,6 +5211,7 @@ class HeritageRAGPipeline(dspy.Module):
# Template-based SPARQL fields
template_used=template_used, # Whether template was used instead of LLM generation
template_id=template_id, # Which template was used (None if LLM fallback)
factual_result=skip_llm_generation, # True if LLM was skipped (factual query fast-path)
)
# Cache the response (fire and forget)