Skip to main content
Glama

Fabric MCP Agent

by yingkiat
direct_mapping_tools.py17.8 kB
""" Direct mapping tools - Fast, deterministic SQL execution for pattern-matched queries """ import re import time from typing import Dict, Any, List from connectors.fabric_dw import execute_sql def execute_competitor_mapping(user_question: str, classification: Dict[str, Any]) -> Dict[str, Any]: """ Direct SQL execution for competitor product mapping using AI-extracted entities This function bypasses AI SQL generation and executes a direct lookup for competitor product equivalents. Uses entities extracted by AI during intent classification. Args: user_question: User question containing competitor products classification: Intent classification result with extracted_entities Returns: dict: Direct mapping results with our equivalent products Raises: ValueError: If required entities are not available in classification Exception: If SQL execution fails (triggers fallback) """ # Extract competitor information from AI classification extracted_entities = classification.get("extracted_entities", {}) competitor_product_raw = extracted_entities.get("competitor_product") competitor_name = extracted_entities.get("competitor_name") if not competitor_product_raw: raise ValueError("No competitor product extracted by AI classification") # Handle multiple products (comma-separated string OR list) if isinstance(competitor_product_raw, list): products = [p.strip() for p in competitor_product_raw if p.strip()] else: products = [p.strip() for p in competitor_product_raw.split(',') if p.strip()] try: start_time = time.time() # Build IN clause for multiple products - much more efficient than individual queries product_list = "', '".join(products) # Join with proper SQL escaping sql = f""" SELECT DISTINCT sdm.競合品番, sdm.品番, sdm.競合メーカー名, sdm.品番2, pt.pt_desc1 + ' ' + pt.pt_desc2 AS 品番説明 FROM JPNMRSdb_SPT_SALES_DRAPE_MAPPING sdm LEFT JOIN JPNPROdb_pt_mstr pt ON sdm.品番 = pt.pt_part WHERE sdm.競合品番 IN ('{product_list}') """ results = execute_sql(sql) execution_time_ms = (time.time() - start_time) * 1000 # Track which products were found vs missing found_products = set() if results: found_products = {row.get('競合品番') for row in results if row.get('競合品番')} successful_products = [p for p in products if p in found_products] failed_products = [p for p in products if p not in found_products] # Try fuzzy matching for failed products if any fuzzy_results = [] if failed_products: for product in failed_products: fuzzy_matches = _try_fuzzy_matching(product) if fuzzy_matches: fuzzy_results.extend(fuzzy_matches) successful_products.append(f"{product} (fuzzy)") failed_products = [p for p in failed_products if p != product] # Remove from failed list # Combine direct and fuzzy results all_results = (results or []) + fuzzy_results return { "success": True, "competitor_products": products, # Multiple products input "competitor_name": competitor_name, "results": all_results, "mapping_type": "multi_product_lookup" if len(products) > 1 else "single_product_lookup", "sql_executed": sql, "execution_time_ms": round(execution_time_ms, 2), "result_count": len(all_results) if all_results else 0, "successful_products": successful_products, "failed_products": failed_products, "extraction_method": "ai_classification" } except Exception as e: # Re-raise to trigger fallback to AI workflow raise Exception(f"Direct mapping SQL execution failed: {str(e)}") # Removed _extract_hogy_product_name - now using AI-extracted entities from classification def _try_fuzzy_matching(product_name: str) -> List[Dict[str, Any]]: """ Fallback fuzzy matching when direct mapping table lookup fails Uses similarity search on product master table """ # Extract key terms from product name for fuzzy search key_terms = _extract_product_keywords(product_name) if not key_terms: return [] # Build dynamic WHERE clause for multiple keyword matching where_conditions = [] params = [] for term in key_terms[:3]: # Use top 3 keywords to avoid too broad search where_conditions.append("(UPPER(product_name_en) LIKE UPPER(?) OR UPPER(product_name_jp) LIKE UPPER(?))") params.extend([f"%{term}%", f"%{term}%"]) fuzzy_sql = f""" SELECT TOP 5 product_id, product_name_en, product_name_jp, specifications, 'fuzzy_match' as mapping_type, 0.7 as estimated_confidence FROM JPNPROdb_ps_mstr WHERE {' OR '.join(where_conditions)} ORDER BY CASE WHEN UPPER(product_name_en) LIKE UPPER(?) THEN 1 WHEN UPPER(product_name_jp) LIKE UPPER(?) THEN 2 ELSE 3 END, product_id """ # Add primary search term for ordering params.extend([f"%{key_terms[0]}%", f"%{key_terms[0]}%"]) try: # Format SQL manually since execute_sql doesn't support parameters formatted_fuzzy_sql = fuzzy_sql for param in params: formatted_fuzzy_sql = formatted_fuzzy_sql.replace("?", f"'{param}'", 1) return execute_sql(formatted_fuzzy_sql) except Exception: # If fuzzy matching also fails, return empty (will trigger AI fallback) return [] def _extract_product_keywords(product_name: str) -> List[str]: """Extract meaningful keywords from product name for fuzzy matching""" # Common medical device keywords that are searchable meaningful_terms = [] # Split and clean product name words = re.split(r'[\s\-\.]+', product_name.lower()) # Filter out common stopwords but keep medical/technical terms stopwords = {'the', 'a', 'an', 'with', 'for', 'and', 'or', 'of'} for word in words: word = word.strip() if word and word not in stopwords and len(word) > 2: meaningful_terms.append(word) # Prioritize technical terms and measurements prioritized = [] technical_indicators = ['ml', 'mm', 'cm', 'gauge', 'fr', 'ch'] for term in meaningful_terms: # Check if term contains technical indicators if any(indicator in term for indicator in technical_indicators): prioritized.insert(0, term) # High priority else: prioritized.append(term) return prioritized[:5] # Return top 5 keywords def execute_product_specs_lookup(user_question: str, classification: Dict[str, Any]) -> Dict[str, Any]: """ Direct SQL execution for product specifications lookup using AI-extracted product codes This function bypasses AI SQL generation and executes a direct lookup for product specifications from JPNMRSdb_SPT_SALES_DRAPE_SPECS table. Args: user_question: User question containing product codes classification: Intent classification result with extracted_entities Returns: dict: Direct specifications results for the requested products Raises: ValueError: If required entities are not available in classification Exception: If SQL execution fails (triggers fallback) """ # Extract product information from AI classification extracted_entities = classification.get("extracted_entities", {}) product_codes_raw = extracted_entities.get("product_codes") if not product_codes_raw: raise ValueError("No product codes extracted by AI classification") # Handle multiple products (comma-separated string OR list) if isinstance(product_codes_raw, list): products = [p.strip() for p in product_codes_raw if p.strip()] else: products = [p.strip() for p in product_codes_raw.split(',') if p.strip()] try: start_time = time.time() # Build IN clause for multiple products - much more efficient than individual queries product_list = "', '".join(products) # Join with proper SQL escaping sql = f""" SELECT 品番 as product_code, -- Product 名称 as description, -- Description of base material AAMIレベル as aami_level, -- AMMI level 横 as width_cm, --Width 縦 as height_cm, --Height 開脚 as legs_split, --Whether the legs are split (Y/N) 素材 as material, --Material of the opening in the middle of the drape 開窓部横 as opening_width_cm, --Width of the opening in the middle of the drape 開窓部縦 as opening_height_cm, --Height of the opening in the middle of the drape 形状 as opening_shape, --Shape of the opening area パウチ as pouch_quantity, --Quantity of pouches [コードホルダ―] as cord_holder, --Cord holder 透明翼 as transparent_panels --Transparent side panels FROM JPNMRSdb_SPT_SALES_DRAPE_SPECS WHERE 品番 IN ('{product_list}') """ results = execute_sql(sql) execution_time_ms = (time.time() - start_time) * 1000 # DEBUG: Log actual SQL and results print(f"DEBUG: Product specs SQL: {sql}") print(f"DEBUG: Raw results count: {len(results) if results else 0}") if results: print(f"DEBUG: First result keys: {list(results[0].keys()) if results else 'No results'}") print(f"DEBUG: First result: {results[0] if results else 'No results'}") # Track which products were found vs missing found_products = set() if results: found_products = {row.get('product_code') for row in results if row.get('product_code')} successful_products = [p for p in products if p in found_products] failed_products = [p for p in products if p not in found_products] return { "success": True, "product_codes": products, # Multiple products input "specifications": results, "lookup_type": "multi_product_specs" if len(products) > 1 else "single_product_specs", "sql_executed": sql, "execution_time_ms": round(execution_time_ms, 2), "result_count": len(results) if results else 0, "successful_products": successful_products, "failed_products": failed_products, "extraction_method": "ai_classification" } except Exception as e: # Re-raise to trigger fallback to AI workflow raise Exception(f"Direct product specs SQL execution failed: {str(e)}") def execute_anz_competitor_mapping(user_question: str, classification: Dict[str, Any]) -> Dict[str, Any]: """ Direct SQL execution for ANZ competitor product mapping using AI-extracted entities This function bypasses AI SQL generation and executes a direct lookup for ANZ competitor product equivalents. Uses entities extracted by AI during intent classification. Args: user_question: User question containing ANZ competitor products classification: Intent classification result with extracted_entities Returns: dict: Direct mapping results with ANZ Medline equivalent products Raises: ValueError: If required entities are not available in classification Exception: If SQL execution fails (triggers fallback) """ # Extract competitor information from AI classification extracted_entities = classification.get("extracted_entities", {}) competitor_product_raw = extracted_entities.get("competitor_product") competitor_name = extracted_entities.get("competitor_name") if not competitor_product_raw: raise ValueError("No competitor product extracted by AI classification") # Handle multiple products (comma-separated string OR list) if isinstance(competitor_product_raw, list): products = [p.strip() for p in competitor_product_raw if p.strip()] else: products = [p.strip() for p in competitor_product_raw.split(',') if p.strip()] try: start_time = time.time() # Build flexible search terms for ANZ competitor mapping search_conditions = [] for product in products: # Create multiple search patterns for each product search_terms = [] # Original product term search_terms.append(f"LOWER(m.[Competitor Product Code] + ' ' + m.[Competitor Description]) LIKE '%{product.lower()}%'") # Split product into key terms for broader matching words = product.replace('-', ' ').replace('.', ' ').split() for word in words: if len(word) > 2: # Skip very short words search_terms.append(f"LOWER(m.[Competitor Product Code] + ' ' + m.[Competitor Description]) LIKE '%{word.lower()}%'") # Combine terms for this product with OR if search_terms: search_conditions.append(f"({' OR '.join(search_terms)})") # Combine all product searches with OR where_clause = ' OR '.join(search_conditions) sql = f""" SELECT m.[Competitor Product Code], m.[Competitor Description], m.[Updated Medline Product Code], m.[Updated Medline Description], m.[Updated Medline Code Status], m.[STD Cost], m.[Category], m.[Sub-Category], m.[BNS Alt Code], m.[FG (Sterile) Alt Code], p.pt_um as [UOM] FROM anz_spt_competitor_mapping m LEFT JOIN ANZPRO2EEdb_pt_mstr p ON m.[Updated Medline Product Code] = p.pt_part WHERE ({where_clause}) AND m.[Updated Medline Code Status] = 'REL' ORDER BY m.[Category], m.[STD Cost] """ results = execute_sql(sql) execution_time_ms = (time.time() - start_time) * 1000 # Track which products were found vs missing found_products = set() if results: # Extract competitor product codes from results to match against input for row in results: comp_code = row.get('Competitor Product Code', '') comp_desc = row.get('Competitor Description', '') full_comp = f"{comp_code} {comp_desc}".lower() # Check which input products match this result for product in products: if product.lower() in full_comp or any(word.lower() in full_comp for word in product.split() if len(word) > 2): found_products.add(product) successful_products = list(found_products) failed_products = [p for p in products if p not in found_products] return { "success": True, "competitor_products": products, "competitor_name": competitor_name, "results": results, "mapping_type": "anz_multi_product_lookup" if len(products) > 1 else "anz_single_product_lookup", "sql_executed": sql, "execution_time_ms": round(execution_time_ms, 2), "result_count": len(results) if results else 0, "successful_products": successful_products, "failed_products": failed_products, "extraction_method": "ai_classification" } except Exception as e: # Re-raise to trigger fallback to AI workflow raise Exception(f"ANZ direct mapping SQL execution failed: {str(e)}") def get_direct_mapping_stats() -> Dict[str, Any]: """Get statistics about direct mapping tool performance (for monitoring)""" # This would integrate with logging system in production return { "tool_name": "competitor_mapping", "supported_competitors": ["Hogy"], "typical_execution_time_ms": 150, "success_rate_target": 0.85, "fallback_strategy": "ai_workflow", "last_updated": "2025-01-01" } def test_competitor_mapping_patterns(): """Test function to validate AI-based entity extraction (replaces regex patterns)""" test_cases = [ "Replace Hogy BD Luer-Lock Syringe 2.5mL with our equivalent", "Hogy catheter equivalent", "Find our version of Hogy surgical kit", "What is the Hogy disposable scalpel equivalent?", "Replace Hogy BD Luer-Lock with domestic product" ] return { "note": "Entity extraction now handled by AI classification in stage0_intent.md", "test_cases": test_cases, "expected_entities": [ {"competitor_name": "BD", "competitor_product": "BD Luer-Lock Syringe 2.5mL"}, {"competitor_name": "Hogy", "competitor_product": "catheter"}, {"competitor_name": "Hogy", "competitor_product": "surgical kit"}, {"competitor_name": "Hogy", "competitor_product": "disposable scalpel"}, {"competitor_name": "Hogy", "competitor_product": "BD Luer-Lock"} ] }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yingkiat/mcp_fabric_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server