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,220 @@
1
+ """
2
+ Business Comparison Service.
3
+
4
+ Compares a target business against one or more competitor businesses across:
5
+ - Website availability & response performance
6
+ - Technical SEO score (0-100)
7
+ - Mobile readiness (viewport configuration)
8
+ - Security (HTTPS & valid SSL)
9
+ - Social media footprint (channels active)
10
+ - Contact accessibility (emails, phone numbers, contact forms)
11
+ - Public metrics (ratings and reviews when available)
12
+
13
+ Generates:
14
+ - Structured side-by-side comparison matrix
15
+ - Concrete competitive strengths and weaknesses
16
+ - High-impact digital opportunities
17
+ - Actionable executive positioning summary
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from typing import Any
24
+
25
+ from .seo_auditor import run_full_seo_audit
26
+ from .website_analyzer import analyze_target_website
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ async def _inspect_business_digital_profile(biz_data: dict[str, Any]) -> dict[str, Any]:
32
+ """Inspect or pull digital intelligence for a single business entity."""
33
+ name = biz_data.get("name") or "Unknown"
34
+ website = biz_data.get("website")
35
+
36
+ profile: dict[str, Any] = {
37
+ "name": name,
38
+ "website": website,
39
+ "category": biz_data.get("category"),
40
+ "city": biz_data.get("city"),
41
+ "is_https": False,
42
+ "website_available": False,
43
+ "response_time_seconds": None,
44
+ "seo_score": 0,
45
+ "mobile_ready": False,
46
+ "social_channels_count": 0,
47
+ "social_channels": [],
48
+ "has_contact_form": False,
49
+ "has_phone": bool(biz_data.get("phone")),
50
+ "has_email": bool(biz_data.get("email")),
51
+ "rating": biz_data.get("rating"),
52
+ "review_count": biz_data.get("review_count"),
53
+ "status": "no_website" if not website else "analyzed",
54
+ }
55
+
56
+ if not website:
57
+ return profile
58
+
59
+ # Run website analysis
60
+ try:
61
+ web_res = await analyze_target_website(website, check_broken_links=False)
62
+ if web_res.get("available"):
63
+ profile["website_available"] = True
64
+ profile["is_https"] = web_res.get("protocol", {}).get("is_https", False)
65
+ profile["response_time_seconds"] = web_res.get("response_time_seconds")
66
+ profile["mobile_ready"] = web_res.get("metadata", {}).get("has_mobile_viewport", False)
67
+ profile["has_contact_form"] = web_res.get("contact_detection", {}).get("has_contact_form", False)
68
+
69
+ if web_res.get("contact_detection", {}).get("emails_found"):
70
+ profile["has_email"] = True
71
+ if web_res.get("contact_detection", {}).get("phones_found"):
72
+ profile["has_phone"] = True
73
+
74
+ # Social channels
75
+ channels = []
76
+ for plat, plist in web_res.get("social_profiles", {}).items():
77
+ if plist:
78
+ channels.append(plat.capitalize())
79
+ profile["social_channels"] = sorted(channels)
80
+ profile["social_channels_count"] = len(channels)
81
+ else:
82
+ profile["status"] = "inaccessible_website"
83
+ profile["error"] = web_res.get("error", "Website unreachable")
84
+ except Exception as e:
85
+ profile["status"] = "error"
86
+ profile["error"] = str(e)
87
+
88
+ # Run SEO audit if website was reachable
89
+ if profile["website_available"]:
90
+ try:
91
+ seo_res = await run_full_seo_audit(website)
92
+ profile["seo_score"] = seo_res.get("seo_score", 0)
93
+ except Exception:
94
+ profile["seo_score"] = 0
95
+
96
+ return profile
97
+
98
+
99
+ async def compare_target_against_competitors(
100
+ target_business: dict[str, Any],
101
+ competitors: list[dict[str, Any]],
102
+ ) -> dict[str, Any]:
103
+ """
104
+ Perform a comparative analysis of target_business against a list of competitors.
105
+ """
106
+ if not target_business or not target_business.get("name"):
107
+ raise ValueError("target_business must contain at least a 'name' field.")
108
+
109
+ if not competitors:
110
+ return {
111
+ "target_business": target_business.get("name"),
112
+ "competitors_compared_count": 0,
113
+ "comparison_matrix": [],
114
+ "strengths": ["No local competitors identified in this category."],
115
+ "weaknesses": [],
116
+ "opportunities": ["First-mover advantage: expand local presence and claim digital listings."],
117
+ "competitive_positioning_summary": f"{target_business.get('name')} has no immediate competitors identified in the provided list.",
118
+ }
119
+
120
+ # Analyze target and competitors
121
+ target_profile = await _inspect_business_digital_profile(target_business)
122
+ competitor_profiles = []
123
+ for comp in competitors:
124
+ comp_profile = await _inspect_business_digital_profile(comp)
125
+ competitor_profiles.append(comp_profile)
126
+
127
+ # Compile Comparison Matrix Rows
128
+ all_profiles = [target_profile] + competitor_profiles
129
+ matrix = []
130
+ for p in all_profiles:
131
+ matrix.append({
132
+ "name": p["name"],
133
+ "role": "Target" if p["name"] == target_profile["name"] else "Competitor",
134
+ "website": p["website"],
135
+ "website_available": "Yes" if p["website_available"] else "No",
136
+ "https_secure": "Yes" if p["is_https"] else "No",
137
+ "seo_score": p["seo_score"],
138
+ "mobile_ready": "Yes" if p["mobile_ready"] else "No",
139
+ "social_channels": ", ".join(p["social_channels"]) if p["social_channels"] else "None",
140
+ "contact_form": "Yes" if p["has_contact_form"] else "No",
141
+ "phone_available": "Yes" if p["has_phone"] else "No",
142
+ "email_available": "Yes" if p["has_email"] else "No",
143
+ "rating": p["rating"] or "N/A",
144
+ })
145
+
146
+ # Competitive Assessment Engine (Strengths / Weaknesses / Opportunities)
147
+ strengths: list[str] = []
148
+ weaknesses: list[str] = []
149
+ opportunities: list[str] = []
150
+
151
+ # 1. SEO Benchmark
152
+ valid_comp_seo = [cp["seo_score"] for cp in competitor_profiles if cp["seo_score"] > 0]
153
+ avg_comp_seo = (sum(valid_comp_seo) / len(valid_comp_seo)) if valid_comp_seo else 0
154
+
155
+ if target_profile["seo_score"] >= 80:
156
+ strengths.append(f"Strong on-page technical SEO score ({target_profile['seo_score']}/100), exceeding competitor average ({round(avg_comp_seo, 1)}/100).")
157
+ elif target_profile["seo_score"] < avg_comp_seo and target_profile["website_available"]:
158
+ weaknesses.append(f"Technical SEO score ({target_profile['seo_score']}/100) lags behind industry rivals (average {round(avg_comp_seo, 1)}/100).")
159
+ opportunities.append("Optimize meta descriptions, canonical links, and H1 tags to close the search visibility gap.")
160
+
161
+ # 2. Website & Performance
162
+ if target_profile["website_available"] and not any(cp["website_available"] for cp in competitor_profiles):
163
+ strengths.append("Maintains an active digital web property while several competitors have no online website.")
164
+ elif not target_profile["website_available"]:
165
+ weaknesses.append("Target business lacks an accessible, active website.")
166
+ opportunities.append("Deploy a modern, mobile-responsive landing page to capture organic leads.")
167
+
168
+ if target_profile["mobile_ready"]:
169
+ strengths.append("Mobile-responsive viewport implemented for smartphone traffic.")
170
+ elif target_profile["website_available"]:
171
+ weaknesses.append("Missing mobile viewport configuration, harming mobile user experience.")
172
+ opportunities.append("Configure a standard viewport meta tag to capture mobile search traffic.")
173
+
174
+ # 3. Security (HTTPS)
175
+ if target_profile["is_https"] and any(not cp["is_https"] and cp["website_available"] for cp in competitor_profiles):
176
+ strengths.append("Fully secured over HTTPS/SSL, outperforming insecure competitor domains.")
177
+ elif not target_profile["is_https"] and target_profile["website_available"]:
178
+ weaknesses.append("Website is not served securely over HTTPS.")
179
+ opportunities.append("Implement free SSL/TLS to boost customer trust and search rank.")
180
+
181
+ # 4. Social Media
182
+ comp_social_counts = [cp["social_channels_count"] for cp in competitor_profiles]
183
+ max_comp_social = max(comp_social_counts) if comp_social_counts else 0
184
+
185
+ if target_profile["social_channels_count"] >= 3 and target_profile["social_channels_count"] >= max_comp_social:
186
+ strengths.append(f"Diversified social presence across {target_profile['social_channels_count']} channels ({', '.join(target_profile['social_channels'])}).")
187
+ elif target_profile["social_channels_count"] < max_comp_social:
188
+ missing_channels = set()
189
+ for cp in competitor_profiles:
190
+ missing_channels.update(cp["social_channels"])
191
+ missing_from_target = missing_channels.difference(target_profile["social_channels"])
192
+ if missing_from_target:
193
+ weaknesses.append(f"Fewer active social channels than competitors (active: {target_profile['social_channels_count']} vs top competitor: {max_comp_social}).")
194
+ opportunities.append(f"Establish brand profiles on {', '.join(sorted(list(missing_from_target)[:3]))} to engage prospective buyers.")
195
+
196
+ # 5. Contact Inbound Funnels
197
+ if target_profile["has_contact_form"] and target_profile["has_email"] and target_profile["has_phone"]:
198
+ strengths.append("Comprehensive contact accessibility: inbound form, email, and direct phone available.")
199
+ else:
200
+ if not target_profile["has_contact_form"] and any(cp["has_contact_form"] for cp in competitor_profiles):
201
+ opportunities.append("Add a direct lead capture / contact form to improve inbound conversion rates.")
202
+ if not target_profile["has_phone"]:
203
+ opportunities.append("Display a verified business phone number for rapid customer inquiries.")
204
+
205
+ # Summary Generation
206
+ summary = (
207
+ f"{target_profile['name']} was benchmarked against {len(competitor_profiles)} industry competitors. "
208
+ f"Target SEO Score: {target_profile['seo_score']}/100 (Competitor average: {round(avg_comp_seo, 1)}/100). "
209
+ f"Identified {len(strengths)} key digital strengths and {len(opportunities)} high-leverage growth opportunities."
210
+ )
211
+
212
+ return {
213
+ "target_business": target_profile["name"],
214
+ "competitors_compared_count": len(competitor_profiles),
215
+ "comparison_matrix": matrix,
216
+ "strengths": strengths or ["Established baseline presence in the local market."],
217
+ "weaknesses": weaknesses or ["No critical structural weaknesses observed."],
218
+ "opportunities": opportunities or ["Maintain current competitive lead and refresh content regularly."],
219
+ "competitive_positioning_summary": summary,
220
+ }
@@ -0,0 +1,294 @@
1
+ """
2
+ Business Report Generation Service.
3
+
4
+ Synthesizes technical audits, SEO evaluations, social presence scans, competitor benchmarks,
5
+ lead scores, and optional AI strategic observations into comprehensive multi-section business intelligence reports.
6
+
7
+ Outputs in Markdown, HTML, or structured JSON across 12 mandated sections:
8
+ 1. Executive Summary
9
+ 2. Business Overview
10
+ 3. Digital Presence Assessment
11
+ 4. Website Technical Audit
12
+ 5. SEO Analysis
13
+ 6. Social Media Analysis
14
+ 7. Competitor Benchmarking
15
+ 8. Lead Qualification
16
+ 9. Identified Digital Opportunities
17
+ 10. Recommended Services
18
+ 11. Suggested Outreach
19
+ 12. Data Sources and Limitations
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import html
25
+ from datetime import datetime
26
+ from typing import Any, Literal, Optional
27
+
28
+ HTML_TEMPLATE = """<!DOCTYPE html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="UTF-8">
32
+ <title>LeadIntel Business Intelligence Report: {business_name}</title>
33
+ <style>
34
+ body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #24292e; max-width: 900px; margin: 40px auto; padding: 0 20px; }}
35
+ h1 {{ color: #1f4e78; border-bottom: 2px solid #e1e4e8; padding-bottom: 12px; }}
36
+ h2 {{ color: #2f5597; margin-top: 32px; border-bottom: 1px solid #eaecef; padding-bottom: 6px; }}
37
+ h3 {{ color: #333; }}
38
+ table {{ width: 100%; border-collapse: collapse; margin: 16px 0; }}
39
+ th, td {{ border: 1px solid #dfe2e5; padding: 10px 12px; text-align: left; }}
40
+ th {{ background-color: #f6f8fa; font-weight: 600; }}
41
+ tr:nth-child(even) {{ background-color: #fafbfc; }}
42
+ .badge {{ display: inline-block; padding: 4px 10px; font-size: 12px; font-weight: bold; border-radius: 4px; }}
43
+ .badge-high {{ background-color: #e2efda; color: #375623; }}
44
+ .badge-good {{ background-color: #d9e1f2; color: #203764; }}
45
+ .badge-med {{ background-color: #fff2cc; color: #7f6000; }}
46
+ .badge-low {{ background-color: #fce4d6; color: #c65911; }}
47
+ .callout {{ background: #f1f8ff; border-left: 4px solid #0366d6; padding: 12px 16px; margin: 16px 0; }}
48
+ .warning {{ background: #fffbdd; border-left: 4px solid #d99b00; padding: 12px 16px; margin: 16px 0; }}
49
+ </style>
50
+ </head>
51
+ <body>
52
+ {body_content}
53
+ </body>
54
+ </html>
55
+ """
56
+
57
+
58
+ def generate_comprehensive_business_report(
59
+ business_info: dict[str, Any],
60
+ website_analysis: Optional[dict[str, Any]] = None,
61
+ seo_audit: Optional[dict[str, Any]] = None,
62
+ social_profiles: Optional[dict[str, Any]] = None,
63
+ competitor_research: Optional[dict[str, Any]] = None,
64
+ lead_score_data: Optional[dict[str, Any]] = None,
65
+ ai_insights: Optional[dict[str, Any]] = None,
66
+ output_format: Literal["markdown", "html", "json"] = "markdown",
67
+ ) -> str | dict[str, Any]:
68
+ """
69
+ Generate an executive business intelligence report across 12 structured sections.
70
+ """
71
+ name = business_info.get("name") or "Unknown Business"
72
+ category = business_info.get("category") or "Unclassified"
73
+ city = business_info.get("city") or "Unspecified"
74
+ website = business_info.get("website")
75
+ ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
76
+
77
+ # Extract score details
78
+ score_val = lead_score_data.get("overall_lead_score", 0.0) if lead_score_data else 0.0
79
+ tier = lead_score_data.get("priority_classification", "Unscored") if lead_score_data else "Unscored"
80
+ rec_package = lead_score_data.get("recommended_service_package", "Digital Growth Consultation") if lead_score_data else "General Assessment"
81
+
82
+ # Extract SEO
83
+ seo_score = seo_audit.get("seo_score", 0) if seo_audit else "Not Audited"
84
+ seo_issues = [iss.get("message") for iss in seo_audit.get("issues", [])] if seo_audit else []
85
+
86
+ # Extract competitors
87
+ comps = competitor_research.get("competitors", []) if competitor_research else []
88
+
89
+ # Extract social
90
+ social_dict = social_profiles.get("profiles", social_profiles) if social_profiles else {}
91
+ active_social = [plat.capitalize() for plat, lst in social_dict.items() if isinstance(lst, list) and len(lst) > 0]
92
+
93
+ # JSON output
94
+ if output_format.lower() == "json":
95
+ return {
96
+ "report_title": f"Business Intelligence Report: {name}",
97
+ "generated_at": ts,
98
+ "section_1_executive_summary": (
99
+ f"{name} operates in the {category} sector ({city}). Overall prospect score: {score_val}/100 ({tier}). "
100
+ f"Primary recommended engagement: {rec_package}."
101
+ ),
102
+ "section_2_business_overview": {
103
+ "name": name,
104
+ "category": category,
105
+ "city": city,
106
+ "website": website,
107
+ "phone": business_info.get("phone"),
108
+ "email": business_info.get("email"),
109
+ },
110
+ "section_3_digital_presence_assessment": {
111
+ "website_active": website_analysis.get("available", False) if website_analysis else False,
112
+ "https_secure": website_analysis.get("protocol", {}).get("is_https", False) if website_analysis else False,
113
+ "mobile_ready": website_analysis.get("metadata", {}).get("has_mobile_viewport", False) if website_analysis else False,
114
+ "active_social_channels": active_social,
115
+ },
116
+ "section_4_website_technical_audit": website_analysis or {"status": "Not Audited"},
117
+ "section_5_seo_analysis": seo_audit or {"status": "Not Audited"},
118
+ "section_6_social_media_analysis": social_dict,
119
+ "section_7_competitor_benchmarking": comps,
120
+ "section_8_lead_qualification": lead_score_data or {"status": "Not Scored"},
121
+ "section_9_identified_digital_opportunities": lead_score_data.get("service_opportunities") if lead_score_data else {},
122
+ "section_10_recommended_services": rec_package,
123
+ "section_11_suggested_outreach": ai_insights.get("outreach_campaign") if ai_insights and "outreach_campaign" in ai_insights else {},
124
+ "section_12_data_sources_and_limitations": {
125
+ "primary_source": business_info.get("source", "OpenStreetMap / Public Web"),
126
+ "attribution": "Data © OpenStreetMap contributors, ODbL 1.0 (where applicable).",
127
+ "limitations": "Analysis based on publicly observable on-page technical data. No internal access or private information scraped.",
128
+ },
129
+ }
130
+
131
+ # Markdown output builder
132
+ md_lines = [
133
+ f"# LeadIntel Intelligence Report: {name}",
134
+ f"**Date Generated:** {ts} | **Source:** {business_info.get('source', 'OpenStreetMap / Public Web')}",
135
+ "",
136
+ "---",
137
+ "",
138
+ "## 1. Executive Summary",
139
+ f"**{name}** is a business in the **{category}** industry located in **{city}**.",
140
+ f"- **Prospect Score:** `{score_val}/100` ({tier})",
141
+ f"- **Recommended Engagement Package:** `{rec_package}`",
142
+ f"- **Primary Finding:** {ai_insights.get('ai_insights', {}).get('executive_summary', 'Established local footprint with identifiable opportunities for digital expansion.') if ai_insights else 'Observable technical gaps present immediate upside for web and search optimizations.'}",
143
+ "",
144
+ "## 2. Business Overview",
145
+ "| Attribute | Value |",
146
+ "|---|---|",
147
+ f"| **Business Name** | {name} |",
148
+ f"| **Category** | {category} |",
149
+ f"| **Location** | {city}, {business_info.get('state', '')} {business_info.get('country', '')} |",
150
+ f"| **Website** | {f'[{website}]({website})' if website else 'Not specified'} |",
151
+ f"| **Phone** | {business_info.get('phone') or 'Not listed'} |",
152
+ f"| **Email** | {business_info.get('email') or 'Not listed'} |",
153
+ "",
154
+ "## 3. Digital Presence Assessment",
155
+ f"- **Website Accessibility:** {'✅ Online & Active' if website_analysis and website_analysis.get('available') else '⚠️ Inactive / No Website'}",
156
+ f"- **HTTPS Security:** {'✅ Secured over HTTPS' if website_analysis and website_analysis.get('protocol', {}).get('is_https') else '⚠️ Insecure / Lacks SSL'}",
157
+ f"- **Mobile Viewport:** {'✅ Mobile Responsive' if website_analysis and website_analysis.get('metadata', {}).get('has_mobile_viewport') else '⚠️ Lacks Mobile Viewport'}",
158
+ f"- **Social Channels:** {', '.join(active_social) if active_social else 'No active social links detected'}",
159
+ "",
160
+ "## 4. Website Technical Audit",
161
+ ]
162
+
163
+ if website_analysis and website_analysis.get("available"):
164
+ meta = website_analysis.get("metadata", {})
165
+ resp_t = website_analysis.get("response_time_seconds", "N/A")
166
+ cd = website_analysis.get("contact_detection", {})
167
+ md_lines.extend([
168
+ f"- **Title Tag:** `{meta.get('title') or 'Missing'}`",
169
+ f"- **Meta Description:** `{meta.get('meta_description') or 'Missing'}`",
170
+ f"- **Response Time:** `{resp_t}s`",
171
+ f"- **Contact Form:** {'Detected' if cd.get('has_contact_form') else 'Not detected'}",
172
+ ])
173
+ else:
174
+ md_lines.append("*Website was not analyzed or is unreachable.*")
175
+
176
+ md_lines.extend([
177
+ "",
178
+ "## 5. SEO Analysis",
179
+ f"- **Technical SEO Score:** `{seo_score}/100`",
180
+ ])
181
+ if seo_issues:
182
+ md_lines.append("- **Key Detected SEO Issues:**")
183
+ for iss in seo_issues[:5]:
184
+ md_lines.append(f" - {iss}")
185
+ else:
186
+ md_lines.append("- *No critical SEO issues flagged.*")
187
+
188
+ md_lines.extend([
189
+ "",
190
+ "## 6. Social Media Analysis",
191
+ f"- **Active Channels:** {len(active_social)} platform(s)",
192
+ f"- **Profiles Detected:** {', '.join(active_social) if active_social else 'None'}",
193
+ "",
194
+ "## 7. Competitor Benchmarking",
195
+ ])
196
+
197
+ if comps:
198
+ md_lines.extend([
199
+ "| Competitor | Type | Category | Website | Confidence |",
200
+ "|---|---|---|---|---|",
201
+ ])
202
+ for c in comps[:5]:
203
+ w_disp = f"[Link]({c.get('website')})" if c.get('website') else "None"
204
+ md_lines.append(
205
+ f"| {c.get('name')} | {c.get('competitor_type', 'indirect').capitalize()} | {c.get('category') or 'N/A'} | {w_disp} | {c.get('confidence_score')} |"
206
+ )
207
+ else:
208
+ md_lines.append("*No local direct competitors benchmarked.*")
209
+
210
+ md_lines.extend([
211
+ "",
212
+ "## 8. Lead Qualification",
213
+ f"- **Composite Score:** `{score_val}/100`",
214
+ f"- **Priority Classification:** `{tier}`",
215
+ f"- **Evidence Completeness:** `{lead_score_data.get('evidence_completeness_percentage', 0) if lead_score_data else 0}%`",
216
+ f"- **Confidence Level:** `{lead_score_data.get('confidence_level', 'Unrated') if lead_score_data else 'Unrated'}`",
217
+ "",
218
+ "## 9. Identified Digital Opportunities",
219
+ ])
220
+
221
+ if lead_score_data and "service_opportunities" in lead_score_data:
222
+ opps = lead_score_data["service_opportunities"]
223
+ for svc, val in opps.items():
224
+ md_lines.append(f"- **{svc.replace('_', ' ').title()}:** Opportunity Score `{val}/100`")
225
+ else:
226
+ md_lines.append("- *Opportunities not scored.*")
227
+
228
+ md_lines.extend([
229
+ "",
230
+ "## 10. Recommended Services",
231
+ f"- **Primary Package:** **{rec_package}**",
232
+ "- **Strategic Rationale:** Tailored to close observable gaps in website performance, technical search visibility, and mobile conversion.",
233
+ "",
234
+ "## 11. Suggested Outreach",
235
+ ])
236
+
237
+ # Outreach drafts
238
+ outreach = ai_insights.get("outreach_campaign") if ai_insights else None
239
+ if outreach:
240
+ em = outreach.get("email_pitch", {})
241
+ li = outreach.get("linkedin_message", {})
242
+ wa = outreach.get("whatsapp_message", {})
243
+ md_lines.extend([
244
+ "### Email Draft",
245
+ f"**Subject:** {em.get('subject', 'Partnership inquiry')}",
246
+ "",
247
+ f"{em.get('body', '')}",
248
+ "",
249
+ "### LinkedIn Connection Message",
250
+ f"> {li.get('message', '')}",
251
+ "",
252
+ "### WhatsApp Message Draft",
253
+ f"> {wa.get('message', '')}",
254
+ ])
255
+ else:
256
+ md_lines.extend([
257
+ f"**Suggested Topic:** Introduce your agency's {rec_package} capabilities referencing the verified technical gaps noted in Section 4.",
258
+ "> *Tip: Use the `generate_outreach` tool with local Ollama to produce multi-channel drafts.*",
259
+ ])
260
+
261
+ md_lines.extend([
262
+ "",
263
+ "## 12. Data Sources and Limitations",
264
+ "- **Data Sources:** OpenStreetMap (ODbL 1.0 license), direct public HTTP inspection, local deterministic heuristic algorithms.",
265
+ "- **Attribution:** Data © OpenStreetMap contributors, ODbL 1.0 (where applicable).",
266
+ "- **Notice & Transparency:** This report is based exclusively on publicly observable on-page technical data. Lead scoring is deterministic and reflects observable digital gaps. It does not speculate on commercial budgets or purchasing intent.",
267
+ ])
268
+
269
+ md_content = "\n".join(md_lines)
270
+
271
+ if output_format.lower() == "html":
272
+ # Convert simple markdown headers and tables to basic HTML with HTML-escaping to prevent XSS
273
+ html_body = []
274
+ for line in md_lines:
275
+ if line.startswith("# "):
276
+ html_body.append(f"<h1>{html.escape(line[2:])}</h1>")
277
+ elif line.startswith("## "):
278
+ html_body.append(f"<h2>{html.escape(line[3:])}</h2>")
279
+ elif line.startswith("### "):
280
+ html_body.append(f"<h3>{html.escape(line[4:])}</h3>")
281
+ elif line.startswith("> "):
282
+ html_body.append(f"<blockquote>{html.escape(line[2:])}</blockquote>")
283
+ elif line.startswith("- "):
284
+ html_body.append(f"<li>{html.escape(line[2:])}</li>")
285
+ elif line.startswith("|") and not line.startswith("|---"):
286
+ parts = [f"<td>{html.escape(p.strip())}</td>" for p in line.strip("|").split("|")]
287
+ html_body.append(f"<tr>{''.join(parts)}</tr>")
288
+ elif line.strip() == "---":
289
+ html_body.append("<hr>")
290
+ elif line.strip():
291
+ html_body.append(f"<p>{html.escape(line)}</p>")
292
+ return HTML_TEMPLATE.format(business_name=html.escape(name), body_content="\n".join(html_body))
293
+
294
+ return md_content