Enhance natural language parsing with synonym support and fuzzy matching - #36
Conversation
…sive tests Co-authored-by: vannu07 <161148074+vannu07@users.noreply.github.com>
Co-authored-by: vannu07 <161148074+vannu07@users.noreply.github.com>
Co-authored-by: vannu07 <161148074+vannu07@users.noreply.github.com>
Co-authored-by: vannu07 <161148074+vannu07@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances Jarvis's natural language parsing capabilities by implementing synonym support and fuzzy matching for command recognition. The parser now handles flexible phrasing, common misspellings, and natural language variations using the fuzzywuzzy library with a 60% similarity threshold.
Key changes:
- Expanded intent mapping from 5 to 13 intents with 120+ phrase variations
- Implemented fuzzy matching using fuzzywuzzy with token_sort_ratio scoring
- Added comprehensive feature handlers for new intents including search, music, news, screenshots, and system commands
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/nlp/command_parser.py | Expanded command_map with 8 new intents and additional phrase variations; lowered fuzzy matching threshold to 60%; added utility functions for intent introspection |
| backend/feature.py | Extended handle_user_text() with handlers for all 13 intents including date queries, Google search with term extraction, screenshot capture, and system commands |
| requirements.txt | Added fuzzywuzzy==0.18.0 and python-Levenshtein==0.21.1 dependencies |
| testing/test_command_parser.py | Added comprehensive test suite with 78 unit tests covering exact matches, synonyms, fuzzy matching, case sensitivity, and edge cases |
| testing/test_integration.py | Added integration tests validating intent coverage, flexibility, and real-world scenarios |
| docs/COMMAND_REFERENCE.md | New documentation detailing all 13 supported commands with examples, variations, and technical details |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| search_words = ["search", "google", "for", "look", "up", "find", "on", "can", "you", "could", "about", "information"] | ||
| search_term = user_text.lower() | ||
| for word in search_words: | ||
| search_term = search_term.replace(word, "") | ||
| search_term = search_term.strip() |
There was a problem hiding this comment.
The search term extraction logic using simple string replacement can cause issues. Using replace() without word boundaries will incorrectly modify words. For example, "search for information" would become "mati" after removing "search", "for", "info", "on". The word "information" would have "for", "on", and "information" removed from it, leaving only partial fragments.
Consider using word boundary matching or tokenization to remove only complete words, not substrings within words.
|
|
||
| if search_term: | ||
| speak(f"Searching Google for {search_term}") | ||
| webbrowser.open(f"https://www.google.com/search?q={search_term}") |
There was a problem hiding this comment.
The search term is not URL-encoded before being passed to the Google search URL. This could cause issues with special characters, spaces, or non-ASCII characters in the search query. While webbrowser.open() may handle some basic cases, explicit URL encoding ensures proper handling of all characters.
Consider using urllib.parse.quote_plus() to properly encode the search term.
| from datetime import datetime | ||
|
|
||
| now = datetime.now().strftime("%H:%M") | ||
| speak(f"The current time is {now}") | ||
|
|
||
| elif intent == "get_date": | ||
| from datetime import datetime | ||
|
|
||
| today = datetime.now().strftime("%B %d, %Y") | ||
| speak(f"Today's date is {today}") | ||
|
|
||
| # Opening applications | ||
| elif intent == "open_youtube": | ||
| PlayYoutube(user_text) | ||
|
|
||
| elif intent in ["open_whatsapp", "open_calculator"]: | ||
| elif intent in ["open_whatsapp", "open_calculator", "open_browser"]: | ||
| openCommand(user_text) | ||
|
|
||
| # Weather | ||
| elif intent == "get_weather": | ||
| speak("Please tell me the city name") | ||
| # Note: Weather fetching requires city name, which should be handled separately | ||
|
|
||
| # Music | ||
| elif intent == "play_music": | ||
| speak("Playing music") | ||
| # Note: Can be extended to integrate with music services | ||
|
|
||
| # News | ||
| elif intent == "get_news": | ||
| speak("Fetching latest news") | ||
| # Note: Can be integrated with news_fetcher module | ||
|
|
||
| # Search | ||
| elif intent == "search_google": | ||
| # Remove common search command words to extract the actual search term | ||
| search_words = ["search", "google", "for", "look", "up", "find", "on", "can", "you", "could", "about", "information"] | ||
| search_term = user_text.lower() | ||
| for word in search_words: | ||
| search_term = search_term.replace(word, "") | ||
| search_term = search_term.strip() | ||
|
|
||
| if search_term: | ||
| speak(f"Searching Google for {search_term}") | ||
| webbrowser.open(f"https://www.google.com/search?q={search_term}") | ||
| else: | ||
| speak("What would you like me to search for?") | ||
|
|
||
| # Screenshot | ||
| elif intent == "take_screenshot": | ||
| try: | ||
| from datetime import datetime |
There was a problem hiding this comment.
The datetime module is imported inside each conditional block where it's used (lines 83, 89, 134). This is inefficient as the same module is imported multiple times during the function's lifecycle. Additionally, since datetime is already a standard library module with minimal overhead, importing it at the module level would be more efficient and follow Python best practices.
Consider moving the datetime import to the top of the file with other imports.
| screenshot.save(filename) | ||
| speak(f"Screenshot saved as {filename}") |
There was a problem hiding this comment.
The screenshot is saved in the current working directory without specifying an absolute path or creating a dedicated directory. This could lead to screenshots being scattered across different directories depending on where the application is run from, making them difficult to locate.
Consider saving screenshots to a specific directory (e.g., "screenshots/" or user's Pictures folder) and creating the directory if it doesn't exist.
| screenshot.save(filename) | |
| speak(f"Screenshot saved as {filename}") | |
| screenshots_dir = os.path.join(os.path.expanduser("~"), "Pictures", "AssistantScreenshots") | |
| os.makedirs(screenshots_dir, exist_ok=True) | |
| file_path = os.path.join(screenshots_dir, filename) | |
| screenshot.save(file_path) | |
| speak(f"Screenshot saved to {file_path}") |
| # if os.name == 'nt': # Windows | ||
| # os.system("shutdown /s /t 1") | ||
| # else: # Unix/Linux/Mac | ||
| # os.system("shutdown -h now") | ||
|
|
||
| elif intent == "restart": | ||
| speak("Are you sure you want to restart? This action cannot be undone.") | ||
| # Note: Actual restart requires user confirmation in production | ||
| # Uncomment the following for actual restart: | ||
| # confirmation = takecommand() | ||
| # if confirmation and "yes" in confirmation.lower(): | ||
| # speak("Restarting the system") | ||
| # if os.name == 'nt': # Windows | ||
| # os.system("shutdown /r /t 1") | ||
| # else: # Unix/Linux/Mac | ||
| # os.system("shutdown -r now") |
There was a problem hiding this comment.
Using os.system() for executing system commands is deprecated and has security implications. The os.system() function is susceptible to shell injection attacks if any user input is passed to it (though in this case the commands are hardcoded). Additionally, the error handling and return value checking with os.system() is less reliable than using subprocess module.
Consider using subprocess.run() instead, which is the modern, safer approach for executing system commands.
| # Lower threshold to 60 for better fuzzy matching of misspellings and variations | ||
| if score >= 60: |
There was a problem hiding this comment.
The fuzzy matching threshold of 60% is quite low and could lead to false positive matches. For example, very short commands or completely unrelated phrases might match with 60% similarity, causing unintended behavior. While this allows for more flexible matching of misspellings, it reduces precision and could cause the parser to incorrectly interpret user commands.
Consider testing edge cases more thoroughly or implementing a dynamic threshold based on input length, or adding a minimum input length requirement to prevent accidental matches.
| "screenshot", | ||
| "capture screen", | ||
| "screen capture", | ||
| "take screenshot", |
There was a problem hiding this comment.
The phrase "take a screenshot" is duplicated at line 122 and line 125. Having duplicate phrases in the same intent list serves no purpose and could cause confusion during maintenance.
Remove the duplicate entry.
| "take screenshot", |
| # if confirmation and "yes" in confirmation.lower(): | ||
| # speak("Shutting down the system") | ||
| # if os.name == 'nt': # Windows | ||
| # os.system("shutdown /s /t 1") | ||
| # else: # Unix/Linux/Mac | ||
| # os.system("shutdown -h now") |
There was a problem hiding this comment.
This comment appears to contain commented-out code.
| # Note: Actual shutdown requires user confirmation in production | ||
| # Uncomment the following for actual shutdown: | ||
| # confirmation = takecommand() | ||
| # if confirmation and "yes" in confirmation.lower(): | ||
| # speak("Shutting down the system") | ||
| # if os.name == 'nt': # Windows | ||
| # os.system("shutdown /s /t 1") | ||
| # else: # Unix/Linux/Mac | ||
| # os.system("shutdown -h now") | ||
|
|
||
| elif intent == "restart": | ||
| speak("Are you sure you want to restart? This action cannot be undone.") | ||
| # Note: Actual restart requires user confirmation in production | ||
| # Uncomment the following for actual restart: | ||
| # confirmation = takecommand() | ||
| # if confirmation and "yes" in confirmation.lower(): | ||
| # speak("Restarting the system") | ||
| # if os.name == 'nt': # Windows | ||
| # os.system("shutdown /r /t 1") | ||
| # else: # Unix/Linux/Mac | ||
| # os.system("shutdown -r now") |
There was a problem hiding this comment.
This comment appears to contain commented-out code.
| # Note: Actual shutdown requires user confirmation in production | |
| # Uncomment the following for actual shutdown: | |
| # confirmation = takecommand() | |
| # if confirmation and "yes" in confirmation.lower(): | |
| # speak("Shutting down the system") | |
| # if os.name == 'nt': # Windows | |
| # os.system("shutdown /s /t 1") | |
| # else: # Unix/Linux/Mac | |
| # os.system("shutdown -h now") | |
| elif intent == "restart": | |
| speak("Are you sure you want to restart? This action cannot be undone.") | |
| # Note: Actual restart requires user confirmation in production | |
| # Uncomment the following for actual restart: | |
| # confirmation = takecommand() | |
| # if confirmation and "yes" in confirmation.lower(): | |
| # speak("Restarting the system") | |
| # if os.name == 'nt': # Windows | |
| # os.system("shutdown /r /t 1") | |
| # else: # Unix/Linux/Mac | |
| # os.system("shutdown -r now") | |
| # Note: Actual shutdown behavior (including user confirmation and | |
| # platform-specific system commands) should be implemented with | |
| # appropriate safeguards in production code, outside this example. | |
| elif intent == "restart": | |
| speak("Are you sure you want to restart? This action cannot be undone.") | |
| # Note: Actual restart behavior (including user confirmation and | |
| # platform-specific system commands) should be implemented with | |
| # appropriate safeguards in production code, outside this example. |
📝 Description
Enhanced command parser to handle flexible phrasing, synonyms, and misspellings using fuzzy matching. Expanded from 5 to 13 intents with 120+ phrase variations.
Key Changes:
Fuzzy matching for misspellings: Lowered threshold to 60% using
fuzzywuzzylibrary with Levenshtein distance"wat time is it"→get_time✅"calculater"→open_calculator✅Expanded intent mappings: Added 8 new intents (date, search, music, news, browser, screenshot, shutdown, restart) with 8-12 synonym variations each
Enhanced feature handlers: Extended
handle_user_text()with action handlers for all intents, including:Example:
✅ Type of Change
🔍 Checklist
💬 Additional Context
Testing:
Dependencies added:
fuzzywuzzy==0.18.0python-Levenshtein==0.21.1Documentation:
docs/COMMAND_REFERENCE.md- Complete reference for all 13 supported intents with usage examplesOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.