From c0d31b390509f4fa1287fa5ae32c494eb191bb52 Mon Sep 17 00:00:00 2001 From: kempersc Date: Fri, 9 Jan 2026 18:26:40 +0100 Subject: [PATCH] fix(rag): add fallback imports for semantic_router and temporal_intent Support both relative and absolute imports for running as module or script. --- backend/rag/dspy_heritage_rag.py | 125 ++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 11 deletions(-) diff --git a/backend/rag/dspy_heritage_rag.py b/backend/rag/dspy_heritage_rag.py index ff297409a8..b89f8ee773 100644 --- a/backend/rag/dspy_heritage_rag.py +++ b/backend/rag/dspy_heritage_rag.py @@ -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)