This solution enables the Rasa chatbot to automatically correct typos and spacing errors in sensor names by extracting sensor mentions from natural language text and mapping them to canonical forms in sensor_list.txt.
Users frequently reference sensors with:
- Spaces instead of underscores: "NO2 Level sensor 5.09" vs
NO2_Level_Sensor_5.09 - Case inconsistencies: "NO2_Level_sensor_5.09" vs
NO2_Level_Sensor_5.09 - Typos: "NO2 Levl Sensor 5.09" vs
NO2_Level_Sensor_5.09 - Missing formatting: "NO2 Level Sensor 5.9" vs
NO2_Level_Sensor_5.09
When these incorrect names reach the NL2SPARQL service, it generates malformed SPARQL queries like:
brick:NO2_Level_Sensor 5.09 # Space before number causes parse errorThe fix has three layers of defense:
Location: ActionQuestionToBrickbot.extract_sensors_from_text()
Detects sensor mentions in natural language using regex patterns:
# Pattern 1: Underscore form (already canonical-ish)
"NO2_Level_Sensor_5.09" -> NO2_Level_Sensor_5.09
# Pattern 2: Space-separated form (natural language)
"NO2 Level Sensor 5.09" -> NO2_Level_Sensor_5.09When: Called in run() method when sensor_type slot is empty.
Location: ActionQuestionToBrickbot.canonicalize_sensor_names()
Maps extracted names to canonical forms using:
- Exact match against
sensor_list.txt - Space/underscore normalization
- RapidFuzz matching with configurable threshold (default 80)
"NO2 Levl Sensor 5.09" -> NO2_Level_Sensor_5.09 (fuzzy score: 92)When: Called after extraction and before NL2SPARQL translation.
Location: ActionQuestionToBrickbot.postprocess_sparql_query()
Fixes residual issues in generated SPARQL:
- Collapses spaces in sensor names:
Sensor 5.09->Sensor_5.09 - Corrects prefixes:
brick:Air_Quality_Sensor_5.01->bldg:Air_Quality_Sensor_5.01
When: Called immediately after receiving SPARQL from NL2SPARQL service.
# Fuzzy matching threshold (0-100)
# Lower = more tolerant, Higher = stricter
FUZZY_THRESHOLD=80
# Auto-reload sensor_list.txt interval (seconds)
SENSOR_LIST_RELOAD_SEC=300
# Path to sensor list file (relative to actions directory)
SENSOR_LIST_FILE=sensor_list.txtThe solution works across all buildings (bldg1, bldg2, bldg3) automatically:
- Each building has its own
sensor_list.txt - Each building's action server loads its own list
- No hardcoded sensor names in the code
- When
sensor_list.txtupdates, changes are auto-reloaded (withinSENSOR_LIST_RELOAD_SEC)
User input: "what is NO2 sensor? what does it measure? where this NO2 Level sensor 5.09 is located?"
┌──────────────────────────────────────────────────────────────┐
│ 1. EXTRACTION │
├──────────────────────────────────────────────────────────────┤
│ Pattern match: "NO2 Level sensor 5.09" │
│ Normalized: "NO2_Level_Sensor_5.09" │
│ Extracted: ["NO2_Level_Sensor_5.09"] │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ 2. CANONICALIZATION │
├──────────────────────────────────────────────────────────────┤
│ Fuzzy match against sensor_list.txt │
│ Match: "NO2_Level_Sensor_5.09" (exact) │
│ Canonical: ["NO2_Level_Sensor_5.09"] │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ 3. QUESTION REWRITE │
├──────────────────────────────────────────────────────────────┤
│ Original: "...where this NO2 Level sensor 5.09 is located?" │
│ Rewritten: "...where this NO2_Level_Sensor_5.09 is located?" │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ 4. NL2SPARQL TRANSLATION │
├──────────────────────────────────────────────────────────────┤
│ Input: { │
│ "question": "...NO2_Level_Sensor_5.09...", │
│ "entity": "bldg:NO2_Level_Sensor_5.09" │
│ } │
│ Output: Correct SPARQL with bldg:NO2_Level_Sensor_5.09 │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ 5. SPARQL POSTPROCESSING (safety net) │
├──────────────────────────────────────────────────────────────┤
│ Fix: "Sensor 5.09" -> "Sensor_5.09" │
│ Fix: "brick:Type_Sensor_#.##" -> "bldg:Type_Sensor_#.##" │
└──────────────────────────────────────────────────────────────┘
↓
EXECUTE SPARQL QUERY ✓
cd rasa-bldg1/actions
python test_sensor_extraction.pyExpected output:
[Test 1]
Input: what is NO2 sensor? what does it measure? where this NO2 Level sensor 5.09 is located?
Extracted: 1 sensor(s)
'NO2 Level sensor 5.09' -> 'NO2_Level_Sensor_5.09'
Rewritten: what is NO2 sensor? what does it measure? where this NO2_Level_Sensor_5.09 is located?
-
Rebuild action server:
docker-compose -f docker-compose.bldg1.yml build action_server_bldg1 docker-compose -f docker-compose.bldg1.yml up -d action_server_bldg1
-
Test via Rasa UI:
- Navigate to
http://localhost:3000 - Send: "what is NO2 sensor? what does it measure? where this NO2 Level sensor 5.09 is located?"
- Verify: No SPARQL parse errors
- Check logs:
docker logs action_server_bldg1 --tail 50
- Navigate to
The solution logs key steps for debugging:
INFO - Extracted sensors from text: original_question='...NO2 Level sensor 5.09...', extracted_sensors=['NO2_Level_Sensor_5.09'], rewritten_question='...NO2_Level_Sensor_5.09...'
INFO - Fuzzy-matched 'NO2_Level_Sensor_5.09' -> 'NO2_Level_Sensor_5.09' (score=100)
INFO - Prepared NL2SPARQL payload: entity_sent='bldg:NO2_Level_Sensor_5.09', question_used='...NO2_Level_Sensor_5.09...'
INFO - SPARQL postprocessing applied corrections
Symptom: Extraction returns empty list
Solution:
- Check sensor name exists in
sensor_list.txt - Lower
FUZZY_THRESHOLD(e.g., 70) - Add more regex patterns to
extract_sensors_from_text()
Symptom: Fuzzy match returns incorrect sensor
Solution:
- Increase
FUZZY_THRESHOLD(e.g., 90) - Check for duplicate/similar names in
sensor_list.txt - Review fuzzy match logs for score details
Symptom: Parse errors after postprocessing
Solution:
- Check NL2SPARQL service logs
- Manually inspect generated SPARQL in logs
- Add additional patterns to
postprocess_sparql_query()
To enable this feature for bldg2/bldg3:
-
Copy the enhanced methods to
rasa-bldg2/actions/actions.py:extract_sensors_from_text()_fuzzy_match_single()canonicalize_sensor_names()(if not already present)postprocess_sparql_query()rewrite_question_with_sensors()
-
Update the run() method integration (lines ~1730-1760):
# After sensor_types = tracker.get_slot("sensor_type") or [] extracted_sensors = [] rewritten_question = user_question if not sensor_types: sensor_mappings = self.extract_sensors_from_text(user_question) if sensor_mappings: extracted_sensors = [canonical for _, _, canonical in sensor_mappings] rewritten_question = self.rewrite_question_with_sensors(user_question, sensor_mappings) sensor_types = extracted_sensors # Update NL2SPARQL payload to use rewritten_question input_data = {"question": rewritten_question, "entity": entity_value} # After receiving sparql_query from NL2SPARQL if sparql_query: sparql_query = self.postprocess_sparql_query(sparql_query)
-
Ensure sensor_list.txt exists in
rasa-bldg2/actions/with building-specific sensors -
Set environment variables in
docker-compose.bldg2.yml:action_server_bldg2: environment: - FUZZY_THRESHOLD=80 - SENSOR_LIST_RELOAD_SEC=300
-
Test thoroughly with building-specific sensor names
- Extraction: ~5-10ms per query (regex + fuzzy matching)
- Canonicalization: ~2-5ms per sensor (cached list, fuzzy threshold 80)
- Postprocessing: <1ms (simple regex replacements)
- Total overhead: ~10-20ms per query (negligible compared to NL2SPARQL/SPARQL execution)
- sensor_list.txt updates: Auto-reloaded within 300s (configurable)
- Threshold tuning: Adjust
FUZZY_THRESHOLDbased on false positive/negative rates - Pattern expansion: Add new regex patterns to
extract_sensors_from_text()as needed - Testing: Run
test_sensor_extraction.pyafter any sensor_list.txt changes