From 6882d7ab71b4ee142e16441a8694438408b439be Mon Sep 17 00:00:00 2001 From: Nitzan P <9297302+nitzpo@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:45:56 +0200 Subject: [PATCH] Add outlier filtering metadata to get_valuation_comparables response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP now includes detailed outlier filtering information in the response, allowing LLMs and bots to inform users about data quality improvements. Changes to nadlan_mcp/fastmcp_server.py: - Updated get_valuation_comparables to include outlier filtering metadata - deal_breakdown now includes when filtering is applied: - total_deals: Count after filtering (final result) - total_deals_before_filtering: Original count before filtering - outliers_removed: Number of deals filtered out - filtering_method: Method used ("iqr", "percent", or "none") - iqr_multiplier: IQR multiplier when using IQR method (e.g., 1.5) - Updated docstring to clarify that outlier filtering is automatic and metadata is included in response Changes to tests/e2e/test_mcp_tools_comprehensive.py: - Added assertions to verify outlier filtering metadata fields - Test now validates the structure and types of filtering information Example response: { "market_statistics": { "deal_breakdown": { "total_deals": 15, "total_deals_before_filtering": 17, "outliers_removed": 2, "filtering_method": "iqr", "iqr_multiplier": 1.5 } } } This allows bots like nadlan-bot to properly inform users: "Found 17 comparable deals, filtered 2 outliers using IQR method (k=1.5), showing 15 deals." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- nadlan_mcp/fastmcp_server.py | 33 ++++++++++++++++++----- tests/e2e/test_mcp_tools_comprehensive.py | 20 +++++++++++--- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/nadlan_mcp/fastmcp_server.py b/nadlan_mcp/fastmcp_server.py index a81ba75..a894acd 100644 --- a/nadlan_mcp/fastmcp_server.py +++ b/nadlan_mcp/fastmcp_server.py @@ -776,8 +776,9 @@ def get_valuation_comparables( """Get comparable properties for valuation analysis. This tool provides detailed comparable deals filtered by your criteria. - Returns a generous number of comparables by default - the LLM analyzing - the results can determine which are most similar based on the full details. + Automatically applies IQR outlier filtering (k=1.5) to remove statistical outliers + and improve data quality. The response includes metadata about filtering so you can + inform users about removed outliers. Args: address: The address to find comparables for (in Hebrew or English) @@ -795,8 +796,14 @@ def get_valuation_comparables( max_comparables: Maximum number of deals to return (default: 50, optimized for MCP token limits) Returns: - JSON string containing filtered comparable deals with full details. - Returns many comparables so LLM can assess similarity and relevance. + JSON string containing: + - Filtered comparable deals with full details + - deal_breakdown with outlier filtering metadata: + - total_deals: Count after filtering + - total_deals_before_filtering: Count before filtering + - outliers_removed: Number of deals filtered out + - filtering_method: Method used (e.g., "iqr") + - iqr_multiplier: IQR multiplier used (e.g., 1.5) """ log_mcp_call( "get_valuation_comparables", @@ -857,6 +864,7 @@ def get_valuation_comparables( # Apply outlier filtering to remove statistical outliers config = get_config() + outlier_report = None if ( config.analysis_outlier_method != "none" and len(filtered_deals) >= config.analysis_min_deals_for_outlier_detection @@ -868,6 +876,19 @@ def get_valuation_comparables( # Calculate statistics on filtered comparables stats = client.calculate_deal_statistics(filtered_deals) + # Build deal breakdown with outlier filtering information + deal_breakdown = { + "total_deals": len(filtered_deals), + } + + # Add outlier filtering metadata if filtering was applied + if outlier_report: + deal_breakdown["total_deals_before_filtering"] = outlier_report["total_deals"] + deal_breakdown["outliers_removed"] = outlier_report["outliers_removed"] + deal_breakdown["filtering_method"] = outlier_report["method_used"] + if outlier_report["method_used"] == "iqr": + deal_breakdown["iqr_multiplier"] = outlier_report["parameters"]["iqr_multiplier"] + # Normalize response structure to match other tools return json.dumps( { @@ -885,9 +906,7 @@ def get_valuation_comparables( }, }, "market_statistics": { - "deal_breakdown": { - "total_deals": len(filtered_deals), - }, + "deal_breakdown": deal_breakdown, "price_statistics": stats.price_statistics, "area_statistics": stats.area_statistics, "price_per_sqm_statistics": stats.price_per_sqm_statistics, diff --git a/tests/e2e/test_mcp_tools_comprehensive.py b/tests/e2e/test_mcp_tools_comprehensive.py index 4e9ebea..7373ab6 100644 --- a/tests/e2e/test_mcp_tools_comprehensive.py +++ b/tests/e2e/test_mcp_tools_comprehensive.py @@ -92,12 +92,24 @@ class TestMCPToolsE2E: # Normalized structure: total_comparables -> market_statistics.deal_breakdown.total_deals assert "market_statistics" in data assert "deal_breakdown" in data["market_statistics"] - assert "total_deals" in data["market_statistics"]["deal_breakdown"] - assert isinstance(data["market_statistics"]["deal_breakdown"]["total_deals"], int) - assert data["market_statistics"]["deal_breakdown"]["total_deals"] >= 0 + deal_breakdown = data["market_statistics"]["deal_breakdown"] + assert "total_deals" in deal_breakdown + assert isinstance(deal_breakdown["total_deals"], int) + assert deal_breakdown["total_deals"] >= 0 + + # Verify outlier filtering metadata is included when filtering is applied + if "outliers_removed" in deal_breakdown: + assert isinstance(deal_breakdown["outliers_removed"], int) + assert deal_breakdown["outliers_removed"] >= 0 + assert "total_deals_before_filtering" in deal_breakdown + assert "filtering_method" in deal_breakdown + assert deal_breakdown["filtering_method"] in ["iqr", "percent", "none"] + if deal_breakdown["filtering_method"] == "iqr": + assert "iqr_multiplier" in deal_breakdown + assert isinstance(deal_breakdown["iqr_multiplier"], (int, float)) # Normalized structure: comparables -> deals - if data["market_statistics"]["deal_breakdown"]["total_deals"] > 0: + if deal_breakdown["total_deals"] > 0: assert "deals" in data comp = data["deals"][0] assert "deal_amount" in comp