Add outlier filtering metadata to get_valuation_comparables response

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 <noreply@anthropic.com>
This commit is contained in:
Nitzan P
2025-11-26 23:45:56 +02:00
parent 623d79947b
commit 6882d7ab71
2 changed files with 42 additions and 11 deletions
+26 -7
View File
@@ -776,8 +776,9 @@ def get_valuation_comparables(
"""Get comparable properties for valuation analysis. """Get comparable properties for valuation analysis.
This tool provides detailed comparable deals filtered by your criteria. This tool provides detailed comparable deals filtered by your criteria.
Returns a generous number of comparables by default - the LLM analyzing Automatically applies IQR outlier filtering (k=1.5) to remove statistical outliers
the results can determine which are most similar based on the full details. and improve data quality. The response includes metadata about filtering so you can
inform users about removed outliers.
Args: Args:
address: The address to find comparables for (in Hebrew or English) 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) max_comparables: Maximum number of deals to return (default: 50, optimized for MCP token limits)
Returns: Returns:
JSON string containing filtered comparable deals with full details. JSON string containing:
Returns many comparables so LLM can assess similarity and relevance. - 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( log_mcp_call(
"get_valuation_comparables", "get_valuation_comparables",
@@ -857,6 +864,7 @@ def get_valuation_comparables(
# Apply outlier filtering to remove statistical outliers # Apply outlier filtering to remove statistical outliers
config = get_config() config = get_config()
outlier_report = None
if ( if (
config.analysis_outlier_method != "none" config.analysis_outlier_method != "none"
and len(filtered_deals) >= config.analysis_min_deals_for_outlier_detection and len(filtered_deals) >= config.analysis_min_deals_for_outlier_detection
@@ -868,6 +876,19 @@ def get_valuation_comparables(
# Calculate statistics on filtered comparables # Calculate statistics on filtered comparables
stats = client.calculate_deal_statistics(filtered_deals) 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 # Normalize response structure to match other tools
return json.dumps( return json.dumps(
{ {
@@ -885,9 +906,7 @@ def get_valuation_comparables(
}, },
}, },
"market_statistics": { "market_statistics": {
"deal_breakdown": { "deal_breakdown": deal_breakdown,
"total_deals": len(filtered_deals),
},
"price_statistics": stats.price_statistics, "price_statistics": stats.price_statistics,
"area_statistics": stats.area_statistics, "area_statistics": stats.area_statistics,
"price_per_sqm_statistics": stats.price_per_sqm_statistics, "price_per_sqm_statistics": stats.price_per_sqm_statistics,
+16 -4
View File
@@ -92,12 +92,24 @@ class TestMCPToolsE2E:
# Normalized structure: total_comparables -> market_statistics.deal_breakdown.total_deals # Normalized structure: total_comparables -> market_statistics.deal_breakdown.total_deals
assert "market_statistics" in data assert "market_statistics" in data
assert "deal_breakdown" in data["market_statistics"] assert "deal_breakdown" in data["market_statistics"]
assert "total_deals" in data["market_statistics"]["deal_breakdown"] deal_breakdown = data["market_statistics"]["deal_breakdown"]
assert isinstance(data["market_statistics"]["deal_breakdown"]["total_deals"], int) assert "total_deals" in deal_breakdown
assert data["market_statistics"]["deal_breakdown"]["total_deals"] >= 0 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 # Normalized structure: comparables -> deals
if data["market_statistics"]["deal_breakdown"]["total_deals"] > 0: if deal_breakdown["total_deals"] > 0:
assert "deals" in data assert "deals" in data
comp = data["deals"][0] comp = data["deals"][0]
assert "deal_amount" in comp assert "deal_amount" in comp