phase 2
This commit is contained in:
+145
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
@@ -13,7 +14,24 @@ from .eiendom_no import (
|
||||
get_unit,
|
||||
search_unit_from_finn_url,
|
||||
)
|
||||
from .service import get_or_fetch_ad, get_or_fetch_eiendom_unit
|
||||
from .formatting import (
|
||||
render_ad,
|
||||
render_comparison,
|
||||
render_diff,
|
||||
render_shortlist,
|
||||
render_similar_units,
|
||||
)
|
||||
from .service import (
|
||||
analyze_ad,
|
||||
analyze_ad_against_comps,
|
||||
compare_ads,
|
||||
find_similar_to_liked,
|
||||
get_new_ads_since_last_run,
|
||||
get_or_fetch_ad,
|
||||
get_or_fetch_eiendom_unit,
|
||||
get_shortlist,
|
||||
save_feedback,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -151,6 +169,132 @@ def finn_decode_unit_vector(unit_vector: str) -> str:
|
||||
return json.dumps({"error": True, "message": str(e)})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Additional analysis and enrichment tools
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Fetch and enrich a single FINN ad with optional Eiendom.no data and comparable units."
|
||||
)
|
||||
)
|
||||
async def finn_analyze_ad(
|
||||
finnkode: str,
|
||||
include_eiendom_no: bool = True,
|
||||
include_similar_units: bool = False,
|
||||
) -> str:
|
||||
"""Analyze and enrich a single FINN ad."""
|
||||
try:
|
||||
result = await analyze_ad(
|
||||
finnkode,
|
||||
include_eiendom_no=include_eiendom_no,
|
||||
include_similar_units=include_similar_units,
|
||||
)
|
||||
return render_ad(result.get("ad", {}), "json")
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing ad {finnkode}: {e}")
|
||||
return json.dumps({"error": True, "message": str(e)})
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Evaluate one FINN listing against comparable recently-sold properties from Eiendom.no."
|
||||
)
|
||||
)
|
||||
async def finn_analyze_ad_against_comps(
|
||||
finnkode: str, listing_status: str = "RECENTLY_SOLD"
|
||||
) -> str:
|
||||
"""Analyze ad against comparable sales."""
|
||||
try:
|
||||
result = await analyze_ad_against_comps(finnkode, listing_status=listing_status)
|
||||
return json.dumps(result, default=str)
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing ad {finnkode} against comps: {e}")
|
||||
return json.dumps({"error": True, "message": str(e)})
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Find properties similar to a listing the user has liked. "
|
||||
"Requires that the user has marked the listing with verdict='liked'."
|
||||
)
|
||||
)
|
||||
async def finn_find_similar_to_liked_ad(
|
||||
finnkode: str, mode: str = "recommendations", listing_status: str = "FOR_SALE"
|
||||
) -> str:
|
||||
"""Find properties similar to a liked ad."""
|
||||
try:
|
||||
result = await find_similar_to_liked(finnkode, mode=mode, listing_status=listing_status)
|
||||
return render_similar_units(result, "json")
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding similar to {finnkode}: {e}")
|
||||
return json.dumps({"error": True, "message": str(e)})
|
||||
|
||||
|
||||
@mcp.tool(description="Compare multiple FINN listings side by side with optional enrichment.")
|
||||
async def finn_compare_ads(
|
||||
finnkoder: list[str],
|
||||
include_eiendom_no: bool = True,
|
||||
include_comps: bool = True,
|
||||
) -> str:
|
||||
"""Compare multiple ads."""
|
||||
try:
|
||||
result = await compare_ads(
|
||||
finnkoder,
|
||||
include_eiendom_no=include_eiendom_no,
|
||||
include_comps=include_comps,
|
||||
)
|
||||
return render_comparison(result, "json")
|
||||
except Exception as e:
|
||||
logger.error(f"Error comparing ads: {e}")
|
||||
return json.dumps({"error": True, "message": str(e)})
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description="Store user feedback (verdict, notes) for a FINN listing. "
|
||||
"Enables similar-unit recommendations and shortlist filtering."
|
||||
)
|
||||
async def finn_save_feedback(finnkode: str, verdict: str, notes: str | None = None) -> str:
|
||||
"""Save user feedback for a listing."""
|
||||
try:
|
||||
result = save_feedback(finnkode, verdict, notes)
|
||||
return json.dumps(result, default=str)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving feedback for {finnkode}: {e}")
|
||||
return json.dumps({"error": True, "message": str(e)})
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description="Fetch the stored shortlist from a previous search run. "
|
||||
"Returns the ranked listings with all enrichment data."
|
||||
)
|
||||
def finn_get_shortlist(run_id: int | None = None, limit: int = 10) -> str:
|
||||
"""Get stored shortlist."""
|
||||
try:
|
||||
result = get_shortlist(run_id, limit)
|
||||
return render_shortlist(result, "json")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching shortlist: {e}")
|
||||
return json.dumps({"error": True, "message": str(e)})
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Detect new, removed, and changed listings in a FINN search URL "
|
||||
"compared to the previous run. Shows price/status diffs on changed listings."
|
||||
)
|
||||
)
|
||||
async def finn_get_new_ads_since_last_run(search_url: str) -> str:
|
||||
"""Get new/removed/changed listings since last run."""
|
||||
try:
|
||||
result = get_new_ads_since_last_run(search_url)
|
||||
return render_diff(result, "json")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching diff: {e}")
|
||||
return json.dumps({"error": True, "message": str(e)})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the FastMCP stdio server."""
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
Reference in New Issue
Block a user