leadintel 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. leadintel/__init__.py +6 -0
  2. leadintel/__main__.py +6 -0
  3. leadintel/config.py +165 -0
  4. leadintel/data/__init__.py +1 -0
  5. leadintel/data/demo.py +315 -0
  6. leadintel/exporters/__init__.py +6 -0
  7. leadintel/exporters/csv_exporter.py +113 -0
  8. leadintel/exporters/excel_exporter.py +123 -0
  9. leadintel/models/__init__.py +6 -0
  10. leadintel/models/business.py +203 -0
  11. leadintel/models/report.py +31 -0
  12. leadintel/providers/__init__.py +51 -0
  13. leadintel/providers/base.py +54 -0
  14. leadintel/providers/demo_provider.py +65 -0
  15. leadintel/providers/overpass_provider.py +1040 -0
  16. leadintel/providers/registry.py +151 -0
  17. leadintel/server.py +61 -0
  18. leadintel/services/__init__.py +48 -0
  19. leadintel/services/ai_business_insights.py +310 -0
  20. leadintel/services/business_comparison.py +220 -0
  21. leadintel/services/business_report.py +294 -0
  22. leadintel/services/competitor_research.py +293 -0
  23. leadintel/services/lead_scoring.py +385 -0
  24. leadintel/services/local_ai.py +186 -0
  25. leadintel/services/robots_checker.py +81 -0
  26. leadintel/services/seo_auditor.py +281 -0
  27. leadintel/services/social_finder.py +119 -0
  28. leadintel/services/url_validator.py +295 -0
  29. leadintel/services/website_analyzer.py +213 -0
  30. leadintel/services/website_opportunity.py +523 -0
  31. leadintel/storage/__init__.py +12 -0
  32. leadintel/storage/db.py +189 -0
  33. leadintel/storage/path_validator.py +118 -0
  34. leadintel/tools/__init__.py +38 -0
  35. leadintel/tools/analyze_website.py +57 -0
  36. leadintel/tools/check_seo.py +64 -0
  37. leadintel/tools/compare_businesses.py +65 -0
  38. leadintel/tools/export_leads.py +112 -0
  39. leadintel/tools/find_business_website.py +66 -0
  40. leadintel/tools/find_competitors.py +75 -0
  41. leadintel/tools/find_social_profiles.py +85 -0
  42. leadintel/tools/generate_business_insights.py +93 -0
  43. leadintel/tools/generate_business_report.py +92 -0
  44. leadintel/tools/generate_outreach.py +153 -0
  45. leadintel/tools/get_business_details.py +96 -0
  46. leadintel/tools/get_saved_leads.py +58 -0
  47. leadintel/tools/score_lead.py +88 -0
  48. leadintel/tools/search_businesses.py +305 -0
  49. leadintel/tools/website_finder.py +147 -0
  50. leadintel-0.3.0.dist-info/METADATA +351 -0
  51. leadintel-0.3.0.dist-info/RECORD +54 -0
  52. leadintel-0.3.0.dist-info/WHEEL +4 -0
  53. leadintel-0.3.0.dist-info/entry_points.txt +3 -0
  54. leadintel-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,66 @@
1
+ """
2
+ MCP Tool: find_business_website
3
+
4
+ Discovers and validates the official website of a business entity.
5
+ Uses OpenStreetMap data first, followed by free web search lookup and validation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional
11
+
12
+ from mcp.server.mcpserver import MCPServer
13
+
14
+ from .website_finder import find_website_for_business
15
+
16
+
17
+ def register(mcp: MCPServer) -> None:
18
+ """Register find_business_website tool on the MCP server."""
19
+
20
+ @mcp.tool()
21
+ async def find_business_website(
22
+ business_name: str,
23
+ location: Optional[str] = None,
24
+ business_id: Optional[str] = None,
25
+ ) -> dict:
26
+ """
27
+ Find and verify the official website of a business.
28
+
29
+ Searches local knowledge providers and free web search (no paid API key required).
30
+ Filters out directories, social media links, and aggregators to isolate the primary domain.
31
+
32
+ Args:
33
+ business_name: Name of the business (e.g. 'Blue Bottle Coffee', 'Acme Corp').
34
+ location: Optional location context (e.g. 'San Francisco', 'London').
35
+ business_id: Optional unique LeadIntel/OSM ID (e.g. 'osm_node_123', 'demo_001').
36
+
37
+ Returns:
38
+ Dict containing:
39
+ - found (bool): True if an official website was identified
40
+ - business_name (str): Input business name
41
+ - location (str|null): Input location
42
+ - website (str|null): Discovered website URL
43
+ - discovery_method (str): Method used ('provider:openstreetmap', 'duckduckgo_lite', etc.)
44
+ - note (str|null): Informative status note
45
+ """
46
+ website_url, method = await find_website_for_business(
47
+ business_name=business_name,
48
+ location=location,
49
+ business_id=business_id,
50
+ )
51
+
52
+ found = bool(website_url)
53
+
54
+ return {
55
+ "found": found,
56
+ "business_name": business_name,
57
+ "location": location,
58
+ "business_id": business_id,
59
+ "website": website_url,
60
+ "discovery_method": method,
61
+ "note": (
62
+ f"Discovered website via {method}."
63
+ if found
64
+ else "Could not locate a verified official website."
65
+ ),
66
+ }
@@ -0,0 +1,75 @@
1
+ """
2
+ MCP Tool: find_competitors
3
+
4
+ Discovers relevant industry competitors using OpenStreetMap and semantic taxonomy matching,
5
+ classifies them as direct vs indirect, and assesses digital footprints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Optional
11
+
12
+ from mcp.server.mcpserver import MCPServer
13
+
14
+ from ..services.competitor_research import find_industry_competitors
15
+
16
+
17
+ def register(mcp: MCPServer) -> None:
18
+ """Register find_competitors tool on the MCP server."""
19
+
20
+ @mcp.tool()
21
+ async def find_competitors(
22
+ business_name: str,
23
+ business_category: Optional[str] = None,
24
+ city: Optional[str] = None,
25
+ state: Optional[str] = None,
26
+ country: Optional[str] = None,
27
+ website: Optional[str] = None,
28
+ max_competitors: int = 5,
29
+ enrich_websites: bool = True,
30
+ ) -> dict[str, Any]:
31
+ """
32
+ Discover and analyze competitors of a target business.
33
+
34
+ Finds industry rivals using OpenStreetMap and semantic taxonomy rules.
35
+ Distinguishes direct competitors (exact specialization overlap) from
36
+ indirect competitors (broader category overlap), explaining confidence levels.
37
+
38
+ Features:
39
+ - Filters out unrelated businesses (does not mistake mere geographical proximity for competition).
40
+ - Identifies competitor websites, contact info, ratings, and reviews.
41
+ - Analyzes digital presence (website status, mobile readiness, social media footprint).
42
+ - Zero paid API keys required; respects OpenStreetMap ODbL attribution.
43
+
44
+ Args:
45
+ business_name: Name of the target business (e.g. 'Acme Tech Solutions', 'Blue Bottle Coffee').
46
+ business_category: Industry/category of the business (e.g. 'Software Development', 'Cafe').
47
+ city: Optional city (e.g. 'San Francisco', 'London').
48
+ state: Optional state/province (e.g. 'CA', 'TX').
49
+ country: Optional country (e.g. 'US', 'UK').
50
+ website: Optional target company website URL.
51
+ max_competitors: Maximum competitors to return (1-20, default 5).
52
+ enrich_websites: If True, tests competitor websites for availability and digital channels.
53
+
54
+ Returns:
55
+ Structured competitor research report.
56
+ """
57
+ limit = max(1, min(int(max_competitors), 20))
58
+ try:
59
+ return await find_industry_competitors(
60
+ business_name=business_name,
61
+ business_category=business_category,
62
+ city=city,
63
+ state=state,
64
+ country=country,
65
+ website=website,
66
+ max_competitors=limit,
67
+ enrich_websites=enrich_websites,
68
+ )
69
+ except Exception as exc:
70
+ return {
71
+ "error": str(exc),
72
+ "target_business": {"name": business_name, "category": business_category},
73
+ "total_competitors_found": 0,
74
+ "competitors": [],
75
+ }
@@ -0,0 +1,85 @@
1
+ """
2
+ MCP Tool: find_social_profiles
3
+
4
+ Scans a company's website to extract and verify official social media profiles.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import httpx
12
+ from mcp.server.mcpserver import MCPServer
13
+
14
+ from ..services.robots_checker import RobotsChecker
15
+ from ..services.social_finder import extract_social_profiles
16
+ from ..services.url_validator import validate_and_normalize_url
17
+
18
+ USER_AGENT = "LeadIntel/0.1 (Business Intelligence Researcher; +https://github.com/example/leadintel; respectful crawler)"
19
+
20
+
21
+ def register(mcp: MCPServer) -> None:
22
+ """Register find_social_profiles tool on the MCP server."""
23
+
24
+ @mcp.tool()
25
+ async def find_social_profiles(
26
+ url: str,
27
+ ) -> dict[str, Any]:
28
+ """
29
+ Extract verified official social media profiles from a business website.
30
+
31
+ Discovers:
32
+ - LinkedIn (company / personal)
33
+ - Facebook
34
+ - Instagram
35
+ - Twitter / X
36
+ - YouTube
37
+ - Pinterest
38
+
39
+ Filters out share/dialog widgets, intent links, and irrelevant external domains.
40
+
41
+ Security & Privacy:
42
+ - Prevents SSRF attacks.
43
+ - Respects robots.txt rules.
44
+ - Only extracts publicly linked B2B / official brand profiles.
45
+
46
+ Args:
47
+ url: The business website URL (e.g. 'https://acme.example').
48
+
49
+ Returns:
50
+ Dict containing detected profiles by platform and total count.
51
+ """
52
+ try:
53
+ norm_url = validate_and_normalize_url(url)
54
+ robots = RobotsChecker()
55
+ can_fetch, reason = await robots.can_fetch(norm_url)
56
+ if not can_fetch:
57
+ return {
58
+ "url": norm_url,
59
+ "status": "blocked_by_robots",
60
+ "error": reason or "Blocked by website robots.txt policy.",
61
+ "profiles": {},
62
+ "total_profiles_found": 0,
63
+ }
64
+
65
+ async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
66
+ resp = await client.get(norm_url, headers={"User-Agent": USER_AGENT})
67
+ resp.raise_for_status()
68
+
69
+ profiles = extract_social_profiles(resp.text, base_url=str(resp.url))
70
+ total_found = sum(len(items) for items in profiles.values())
71
+
72
+ return {
73
+ "url": str(resp.url),
74
+ "status": "success",
75
+ "profiles": profiles,
76
+ "total_profiles_found": total_found,
77
+ }
78
+ except Exception as exc:
79
+ return {
80
+ "url": url,
81
+ "status": "error",
82
+ "error": str(exc),
83
+ "profiles": {},
84
+ "total_profiles_found": 0,
85
+ }
@@ -0,0 +1,93 @@
1
+ """
2
+ MCP Tool: generate_business_insights
3
+
4
+ Generates strategic AI business intelligence and opportunity explanations
5
+ using a local Ollama model without external cloud API dependencies.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Optional
11
+
12
+ from mcp.server.mcpserver import MCPServer
13
+
14
+ from ..services.ai_business_insights import generate_ai_business_insights
15
+ from ..services.local_ai import LocalAIClient, OllamaError, OllamaUnavailableError
16
+
17
+
18
+ def register(mcp: MCPServer) -> None:
19
+ """Register generate_business_insights tool on the MCP server."""
20
+
21
+ @mcp.tool()
22
+ async def generate_business_insights(
23
+ business_name: str,
24
+ business_category: Optional[str] = None,
25
+ website_analysis: Optional[dict[str, Any]] = None,
26
+ seo_audit: Optional[dict[str, Any]] = None,
27
+ social_profiles: Optional[dict[str, Any]] = None,
28
+ competitor_comparison: Optional[dict[str, Any]] = None,
29
+ lead_score_data: Optional[dict[str, Any]] = None,
30
+ contact_info: Optional[dict[str, Any]] = None,
31
+ model: Optional[str] = None,
32
+ ) -> dict[str, Any]:
33
+ """
34
+ Generate evidence-based strategic business insights using local Ollama AI.
35
+
36
+ Synthesizes technical audits, competitor benchmarks, and lead scores to provide:
37
+ - Executive summary of digital maturity
38
+ - Clear explanations for digital marketing and SEO opportunities
39
+ - Strategic interpretation of the deterministic lead score
40
+ - Grounded competitor insights
41
+ - Actionable service recommendations (Web Dev, SEO, Social, Marketing)
42
+
43
+ Ethical & Operational Safeguards:
44
+ - Connects to local Ollama (http://localhost:11434). Zero cloud AI calls or tokens used.
45
+ - Grounded strictly in provided evidence; never invents revenue, budgets, or purchasing intent.
46
+ - If Ollama is not installed or running, returns a clean informative error without crashing.
47
+
48
+ Args:
49
+ business_name: Name of the business (required).
50
+ business_category: Industry/category description.
51
+ website_analysis: Results from analyze_website tool (optional).
52
+ seo_audit: Results from check_seo tool (optional).
53
+ social_profiles: Results from find_social_profiles tool (optional).
54
+ competitor_comparison: Results from compare_businesses tool (optional).
55
+ lead_score_data: Results from score_lead tool (optional).
56
+ contact_info: Available contact details (optional).
57
+ model: Optional Ollama model override (default: configured OLLAMA_MODEL, e.g. 'llama3').
58
+
59
+ Returns:
60
+ Structured AI insights grounded in verified facts.
61
+ """
62
+ client = LocalAIClient(model=model) if model else None
63
+ try:
64
+ return await generate_ai_business_insights(
65
+ business_name=business_name,
66
+ business_category=business_category,
67
+ website_analysis=website_analysis,
68
+ seo_audit=seo_audit,
69
+ social_profiles=social_profiles,
70
+ competitor_comparison=competitor_comparison,
71
+ lead_score_data=lead_score_data,
72
+ contact_info=contact_info,
73
+ ai_client=client,
74
+ )
75
+ except OllamaUnavailableError as e:
76
+ return {
77
+ "error": "Ollama server is unavailable.",
78
+ "message": str(e),
79
+ "business_name": business_name,
80
+ "suggestion": "Ensure Ollama is installed and running locally with 'ollama serve'.",
81
+ }
82
+ except OllamaError as e:
83
+ return {
84
+ "error": "Ollama generation failed.",
85
+ "message": str(e),
86
+ "business_name": business_name,
87
+ }
88
+ except Exception as exc:
89
+ return {
90
+ "error": "Failed to generate business insights.",
91
+ "message": str(exc),
92
+ "business_name": business_name,
93
+ }
@@ -0,0 +1,92 @@
1
+ """
2
+ MCP Tool: generate_business_report
3
+
4
+ Synthesizes complete business intelligence into comprehensive multi-section reports
5
+ available in Markdown, HTML, or structured JSON.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Literal, Optional
11
+
12
+ from mcp.server.mcpserver import MCPServer
13
+
14
+ from ..services.business_report import generate_comprehensive_business_report
15
+
16
+
17
+ def register(mcp: MCPServer) -> None:
18
+ """Register generate_business_report tool on the MCP server."""
19
+
20
+ @mcp.tool()
21
+ async def generate_business_report(
22
+ business_info: dict[str, Any],
23
+ website_analysis: Optional[dict[str, Any]] = None,
24
+ seo_audit: Optional[dict[str, Any]] = None,
25
+ social_profiles: Optional[dict[str, Any]] = None,
26
+ competitor_research: Optional[dict[str, Any]] = None,
27
+ lead_score_data: Optional[dict[str, Any]] = None,
28
+ ai_insights: Optional[dict[str, Any]] = None,
29
+ output_format: Literal["markdown", "html", "json"] = "markdown",
30
+ ) -> dict[str, Any]:
31
+ """
32
+ Generate a comprehensive 12-section business intelligence report.
33
+
34
+ Synthesizes technical audits, SEO evaluations, social presence scans,
35
+ competitor benchmarks, lead scores, and optional AI strategic insights.
36
+
37
+ Report Sections:
38
+ 1. Executive Summary
39
+ 2. Business Overview
40
+ 3. Digital Presence Assessment
41
+ 4. Website Technical Audit
42
+ 5. SEO Analysis
43
+ 6. Social Media Analysis
44
+ 7. Competitor Benchmarking
45
+ 8. Lead Qualification & Scoring
46
+ 9. Identified Digital Opportunities
47
+ 10. Recommended Service Packages
48
+ 11. Suggested Multi-Channel Outreach
49
+ 12. Data Sources, Attributions & Limitations
50
+
51
+ Features:
52
+ - Works completely offline without AI or external paid APIs.
53
+ - Outputs in Markdown, HTML, or structured JSON.
54
+ - Accurately references OpenStreetMap ODbL attributions.
55
+
56
+ Args:
57
+ business_info: Dict containing at least 'name', plus optional 'category', 'city', 'website'.
58
+ website_analysis: Output from analyze_website tool (optional).
59
+ seo_audit: Output from check_seo tool (optional).
60
+ social_profiles: Output from find_social_profiles tool (optional).
61
+ competitor_research: Output from find_competitors tool (optional).
62
+ lead_score_data: Output from score_lead tool (optional).
63
+ ai_insights: Output from generate_business_insights or generate_outreach tool (optional).
64
+ output_format: 'markdown' (default), 'html', or 'json'.
65
+
66
+ Returns:
67
+ Dict containing the rendered report content, format, and metadata.
68
+ """
69
+ try:
70
+ report_result = generate_comprehensive_business_report(
71
+ business_info=business_info,
72
+ website_analysis=website_analysis,
73
+ seo_audit=seo_audit,
74
+ social_profiles=social_profiles,
75
+ competitor_research=competitor_research,
76
+ lead_score_data=lead_score_data,
77
+ ai_insights=ai_insights,
78
+ output_format=output_format,
79
+ )
80
+
81
+ return {
82
+ "business_name": business_info.get("name"),
83
+ "format": output_format.lower(),
84
+ "report": report_result,
85
+ }
86
+ except Exception as exc:
87
+ return {
88
+ "error": str(exc),
89
+ "business_name": business_info.get("name") if isinstance(business_info, dict) else str(business_info),
90
+ "format": output_format.lower(),
91
+ "report": f"Report generation failed: {exc}",
92
+ }
@@ -0,0 +1,153 @@
1
+ """
2
+ MCP Tool: generate_outreach
3
+
4
+ Synthesizes tailored, respectful B2B outreach messaging (Email, LinkedIn, WhatsApp)
5
+ using local Ollama models grounded in verified technical findings and website opportunities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Optional
12
+
13
+ from mcp.server.mcpserver import MCPServer
14
+
15
+ from ..models.business import Business
16
+ from ..services.ai_business_insights import generate_ai_outreach
17
+ from ..services.local_ai import LocalAIClient, OllamaError, OllamaUnavailableError
18
+ from ..storage.db import get_business
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def register(mcp: MCPServer) -> None:
24
+ """Register generate_outreach tool on the MCP server."""
25
+
26
+ @mcp.tool()
27
+ async def generate_outreach(
28
+ business_name: str,
29
+ business_id: Optional[str] = None,
30
+ business: Optional[dict[str, Any]] = None,
31
+ business_category: Optional[str] = None,
32
+ website_status: Optional[str] = None,
33
+ website_opportunity_score: Optional[float] = None,
34
+ website_opportunity_explanation: Optional[str] = None,
35
+ detected_cms: Optional[str] = None,
36
+ has_contact_route: Optional[bool] = None,
37
+ website_analysis: Optional[dict[str, Any]] = None,
38
+ seo_audit: Optional[dict[str, Any]] = None,
39
+ social_profiles: Optional[dict[str, Any]] = None,
40
+ competitor_comparison: Optional[dict[str, Any]] = None,
41
+ lead_score_data: Optional[dict[str, Any]] = None,
42
+ contact_info: Optional[dict[str, Any]] = None,
43
+ agency_name: str = "Our Agency",
44
+ sender_name: str = "Lead Specialist",
45
+ model: Optional[str] = None,
46
+ ) -> dict[str, Any]:
47
+ """
48
+ Generate personalized, multi-channel B2B outreach messages using local Ollama AI.
49
+
50
+ Produces ready-to-use drafts across 3 channels:
51
+ - Email pitch: Structured subject line + 3-paragraph value-first pitch referencing verified findings.
52
+ - LinkedIn message: Concise connection request (< 300 characters).
53
+ - WhatsApp Business: Respectful, direct conversational pitch (< 120 words).
54
+
55
+ Ethical & Operational Safeguards:
56
+ - Uses local Ollama model (no third-party cloud data transfer).
57
+ - Grounded exclusively in verified technical data.
58
+ - Does not invent fake claims, guarantees, or assume prospect budgets.
59
+ - Strict guardrails: "Do not fabricate business data or website problems. Base the outreach strictly on the provided website_opportunity_explanation. If the site is missing (website_not_found_after_research), pitch a new build. If a CMS is detected or a contact route is missing, mention it as a specific reason for outreach."
60
+
61
+ Args:
62
+ business_name: Target company name (required).
63
+ business_id: Optional business ID to automatically retrieve stored business from SQLite.
64
+ business: Optional dictionary representation of a Business instance.
65
+ business_category: Business industry/category.
66
+ website_status: Status of the business website (e.g. 'website_not_found_after_research').
67
+ website_opportunity_score: Deterministic 0–100 score prioritizing website opportunity.
68
+ website_opportunity_explanation: Evidence-based explanation of the website opportunity.
69
+ detected_cms: Detected CMS (WordPress, Shopify, Wix, Squarespace).
70
+ has_contact_route: Boolean indicating presence of contact form/mailto route.
71
+ website_analysis: Results from analyze_website tool (optional).
72
+ seo_audit: Results from check_seo tool (optional).
73
+ social_profiles: Results from find_social_profiles tool (optional).
74
+ competitor_comparison: Results from compare_businesses tool (optional).
75
+ lead_score_data: Results from score_lead tool (optional).
76
+ contact_info: Contact details dictionary (optional).
77
+ agency_name: Your digital agency or company name.
78
+ sender_name: Name of the person/role sending the message.
79
+ model: Optional Ollama model override.
80
+
81
+ Returns:
82
+ Dict containing multi-channel outreach drafts.
83
+ """
84
+ # Retrieve or reconstruct Business object if available
85
+ biz_obj: Optional[Business] = None
86
+ if business_id:
87
+ biz_obj = get_business(business_id)
88
+ elif business:
89
+ try:
90
+ biz_obj = Business.model_validate(business)
91
+ except Exception:
92
+ pass
93
+ if not biz_obj and business_name:
94
+ biz_obj = get_business(business_name)
95
+
96
+ if biz_obj:
97
+ if not business_category and biz_obj.category:
98
+ business_category = biz_obj.category
99
+ website_status = website_status or biz_obj.website_status
100
+ if website_opportunity_score is None:
101
+ website_opportunity_score = biz_obj.website_opportunity_score
102
+ website_opportunity_explanation = (
103
+ website_opportunity_explanation or biz_obj.website_opportunity_explanation
104
+ )
105
+ detected_cms = detected_cms or biz_obj.detected_cms
106
+ if has_contact_route is None:
107
+ has_contact_route = biz_obj.has_contact_route
108
+ if not contact_info:
109
+ contact_info = {
110
+ "city": biz_obj.city,
111
+ "phone": biz_obj.phone,
112
+ "email": biz_obj.email,
113
+ }
114
+
115
+ client = LocalAIClient(model=model) if model else None
116
+ try:
117
+ return await generate_ai_outreach(
118
+ business_name=business_name,
119
+ business_category=business_category,
120
+ website_status=website_status,
121
+ website_opportunity_score=website_opportunity_score,
122
+ website_opportunity_explanation=website_opportunity_explanation,
123
+ detected_cms=detected_cms,
124
+ has_contact_route=has_contact_route,
125
+ website_analysis=website_analysis,
126
+ seo_audit=seo_audit,
127
+ social_profiles=social_profiles,
128
+ competitor_comparison=competitor_comparison,
129
+ lead_score_data=lead_score_data,
130
+ contact_info=contact_info,
131
+ agency_name=agency_name,
132
+ sender_name=sender_name,
133
+ ai_client=client,
134
+ )
135
+ except OllamaUnavailableError as e:
136
+ return {
137
+ "error": "Ollama server is unavailable.",
138
+ "message": str(e),
139
+ "business_name": business_name,
140
+ "suggestion": "Ensure Ollama is installed and running locally with 'ollama serve'.",
141
+ }
142
+ except OllamaError as e:
143
+ return {
144
+ "error": "Ollama outreach generation failed.",
145
+ "message": str(e),
146
+ "business_name": business_name,
147
+ }
148
+ except Exception as exc:
149
+ return {
150
+ "error": "Failed to generate outreach drafts.",
151
+ "message": str(exc),
152
+ "business_name": business_name,
153
+ }
@@ -0,0 +1,96 @@
1
+ """
2
+ MCP Tool: get_business_details
3
+
4
+ Fetches complete and enriched information for a single business entity,
5
+ using OpenStreetMap / Overpass data, local demo fallback, and website resolution.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from mcp.server.mcpserver import MCPServer
11
+
12
+ from ..config import settings
13
+ from ..models.business import Business
14
+ from ..providers import build_registry
15
+ from .website_finder import find_website_for_business
16
+
17
+ OSM_ATTRIBUTION = "Data © OpenStreetMap contributors, ODbL 1.0 — https://www.openstreetmap.org/copyright"
18
+
19
+ _registry = build_registry(settings)
20
+
21
+
22
+ def register(mcp: MCPServer) -> None:
23
+ """Register get_business_details tool on the MCP server."""
24
+
25
+ @mcp.tool()
26
+ async def get_business_details(
27
+ business_id: str,
28
+ auto_enrich_website: bool = True,
29
+ ) -> dict:
30
+ """
31
+ Retrieve enriched details for a specific business by ID.
32
+
33
+ Looks up the entity across all available providers (OpenStreetMap, demo fixtures).
34
+ If the business is missing a website and `auto_enrich_website` is True,
35
+ attempts to discover its official website.
36
+
37
+ Args:
38
+ business_id: Unique identifier (e.g. 'osm_node_12345', 'osm_way_67890', or 'demo_001').
39
+ auto_enrich_website: If True, search for missing official website if not present in OSM tags.
40
+
41
+ Returns:
42
+ Dict containing:
43
+ - found (bool): Whether the business was resolved
44
+ - business (dict|null): Enriched business record
45
+ - provider (str): Source of the data
46
+ - attribution (str|null): Mandatory attribution notice if source is OSM
47
+ - enriched_fields (list[str]): Fields added or updated via enrichment
48
+
49
+ Compliance:
50
+ - Respects OSM ODbL attribution.
51
+ - Does not scrape private personal information.
52
+ """
53
+ enriched_fields: list[str] = []
54
+
55
+ biz, provider_name = await _registry.get_details(business_id)
56
+
57
+ if not biz:
58
+ return {
59
+ "found": False,
60
+ "business_id": business_id,
61
+ "business": None,
62
+ "provider": provider_name,
63
+ "attribution": None,
64
+ "enriched_fields": [],
65
+ "note": f"Business ID '{business_id}' not found.",
66
+ }
67
+
68
+ # Auto-enrich website if absent
69
+ if auto_enrich_website and not biz.website and biz.name:
70
+ discovered_url, _ = await find_website_for_business(
71
+ business_name=biz.name,
72
+ location=biz.city,
73
+ business_id=biz.id,
74
+ )
75
+ if discovered_url:
76
+ # Update website field
77
+ biz_dict = biz.model_dump()
78
+ biz_dict["website"] = discovered_url
79
+ biz = Business(**biz_dict)
80
+ enriched_fields.append("website")
81
+
82
+ is_osm = provider_name == "openstreetmap" or biz.source == "openstreetmap"
83
+ attribution = OSM_ATTRIBUTION if is_osm else None
84
+
85
+ return {
86
+ "found": True,
87
+ "business_id": business_id,
88
+ "business": biz.model_dump(mode="json"),
89
+ "provider": provider_name,
90
+ "attribution": attribution,
91
+ "enriched_fields": enriched_fields,
92
+ "note": (
93
+ "Enriched via LeadIntel free intelligence. "
94
+ + (f"Attribution: {OSM_ATTRIBUTION}" if is_osm else "")
95
+ ).strip(),
96
+ }