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.
- leadintel/__init__.py +6 -0
- leadintel/__main__.py +6 -0
- leadintel/config.py +165 -0
- leadintel/data/__init__.py +1 -0
- leadintel/data/demo.py +315 -0
- leadintel/exporters/__init__.py +6 -0
- leadintel/exporters/csv_exporter.py +113 -0
- leadintel/exporters/excel_exporter.py +123 -0
- leadintel/models/__init__.py +6 -0
- leadintel/models/business.py +203 -0
- leadintel/models/report.py +31 -0
- leadintel/providers/__init__.py +51 -0
- leadintel/providers/base.py +54 -0
- leadintel/providers/demo_provider.py +65 -0
- leadintel/providers/overpass_provider.py +1040 -0
- leadintel/providers/registry.py +151 -0
- leadintel/server.py +61 -0
- leadintel/services/__init__.py +48 -0
- leadintel/services/ai_business_insights.py +310 -0
- leadintel/services/business_comparison.py +220 -0
- leadintel/services/business_report.py +294 -0
- leadintel/services/competitor_research.py +293 -0
- leadintel/services/lead_scoring.py +385 -0
- leadintel/services/local_ai.py +186 -0
- leadintel/services/robots_checker.py +81 -0
- leadintel/services/seo_auditor.py +281 -0
- leadintel/services/social_finder.py +119 -0
- leadintel/services/url_validator.py +295 -0
- leadintel/services/website_analyzer.py +213 -0
- leadintel/services/website_opportunity.py +523 -0
- leadintel/storage/__init__.py +12 -0
- leadintel/storage/db.py +189 -0
- leadintel/storage/path_validator.py +118 -0
- leadintel/tools/__init__.py +38 -0
- leadintel/tools/analyze_website.py +57 -0
- leadintel/tools/check_seo.py +64 -0
- leadintel/tools/compare_businesses.py +65 -0
- leadintel/tools/export_leads.py +112 -0
- leadintel/tools/find_business_website.py +66 -0
- leadintel/tools/find_competitors.py +75 -0
- leadintel/tools/find_social_profiles.py +85 -0
- leadintel/tools/generate_business_insights.py +93 -0
- leadintel/tools/generate_business_report.py +92 -0
- leadintel/tools/generate_outreach.py +153 -0
- leadintel/tools/get_business_details.py +96 -0
- leadintel/tools/get_saved_leads.py +58 -0
- leadintel/tools/score_lead.py +88 -0
- leadintel/tools/search_businesses.py +305 -0
- leadintel/tools/website_finder.py +147 -0
- leadintel-0.3.0.dist-info/METADATA +351 -0
- leadintel-0.3.0.dist-info/RECORD +54 -0
- leadintel-0.3.0.dist-info/WHEEL +4 -0
- leadintel-0.3.0.dist-info/entry_points.txt +3 -0
- leadintel-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider registry — selects and chains providers at runtime.
|
|
3
|
+
|
|
4
|
+
Priority order (highest → lowest):
|
|
5
|
+
1. OverpassProvider (free, OSM-backed, requires network)
|
|
6
|
+
2. DemoProvider (always available, synthetic data)
|
|
7
|
+
|
|
8
|
+
Future providers (OpenStreetMapNominatim, custom) slot in without touching
|
|
9
|
+
any tool code.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
import logging
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from ..models.business import Business
|
|
19
|
+
from .base import BusinessProvider
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProviderRegistry:
|
|
28
|
+
"""Ordered list of providers with automatic fallback."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, providers: list[BusinessProvider]) -> None:
|
|
31
|
+
if not providers:
|
|
32
|
+
raise ValueError("ProviderRegistry requires at least one provider.")
|
|
33
|
+
self._providers = providers
|
|
34
|
+
self.last_error: str | None = None
|
|
35
|
+
self.last_search_center: dict[str, float] | None = None
|
|
36
|
+
self.last_radius_meters: int | None = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def primary(self) -> BusinessProvider:
|
|
40
|
+
return self._providers[0]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def all_providers(self) -> list[BusinessProvider]:
|
|
44
|
+
return list(self._providers)
|
|
45
|
+
|
|
46
|
+
def provider_names(self) -> list[str]:
|
|
47
|
+
return [p.name for p in self._providers]
|
|
48
|
+
|
|
49
|
+
async def search(
|
|
50
|
+
self,
|
|
51
|
+
query: str,
|
|
52
|
+
location: str | None = None,
|
|
53
|
+
category: str | None = None,
|
|
54
|
+
limit: int = 10,
|
|
55
|
+
**kwargs: Any,
|
|
56
|
+
) -> tuple[list[Business], str]:
|
|
57
|
+
"""
|
|
58
|
+
Try providers in order, return first non-empty result.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
(businesses, provider_name) — the name of the provider that
|
|
62
|
+
actually returned data.
|
|
63
|
+
"""
|
|
64
|
+
self.last_error = None
|
|
65
|
+
self.last_search_center = None
|
|
66
|
+
self.last_radius_meters = None
|
|
67
|
+
|
|
68
|
+
for provider in self._providers:
|
|
69
|
+
# If a location or explicit coords were provided for live search, do not fall back to demo data
|
|
70
|
+
has_explicit_loc = bool(location or kwargs.get("latitude") is not None)
|
|
71
|
+
if has_explicit_loc and provider.name == "demo" and len(self._providers) > 1:
|
|
72
|
+
logger.debug(
|
|
73
|
+
"Skipping DemoProvider for live search with location '%s'.", location
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
sig = inspect.signature(provider.search)
|
|
79
|
+
if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
|
|
80
|
+
call_kwargs = kwargs
|
|
81
|
+
else:
|
|
82
|
+
call_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters}
|
|
83
|
+
|
|
84
|
+
results = await provider.search(
|
|
85
|
+
query=query,
|
|
86
|
+
location=location,
|
|
87
|
+
category=category,
|
|
88
|
+
limit=limit,
|
|
89
|
+
**call_kwargs,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if hasattr(provider, "last_search_center"):
|
|
93
|
+
self.last_search_center = getattr(provider, "last_search_center", None)
|
|
94
|
+
if hasattr(provider, "last_radius_meters"):
|
|
95
|
+
self.last_radius_meters = getattr(provider, "last_radius_meters", None)
|
|
96
|
+
|
|
97
|
+
if results:
|
|
98
|
+
logger.debug(
|
|
99
|
+
"Provider '%s' returned %d result(s).",
|
|
100
|
+
provider.name,
|
|
101
|
+
len(results),
|
|
102
|
+
)
|
|
103
|
+
return results, provider.name
|
|
104
|
+
|
|
105
|
+
# If a live provider successfully executed search and found 0 results for a location,
|
|
106
|
+
# do not fall through to demo data — report 0 live results.
|
|
107
|
+
if has_explicit_loc and provider.name != "demo":
|
|
108
|
+
logger.debug(
|
|
109
|
+
"Live provider '%s' returned 0 results for location '%s'.",
|
|
110
|
+
provider.name,
|
|
111
|
+
location,
|
|
112
|
+
)
|
|
113
|
+
return [], provider.name
|
|
114
|
+
|
|
115
|
+
logger.debug("Provider '%s' returned 0 results, trying next.", provider.name)
|
|
116
|
+
except Exception as exc: # noqa: BLE001
|
|
117
|
+
self.last_error = str(exc)
|
|
118
|
+
logger.warning("Provider '%s' failed: %s", provider.name, exc)
|
|
119
|
+
# In live mode with a location, do not swallow the error into demo data
|
|
120
|
+
if has_explicit_loc and provider.name != "demo":
|
|
121
|
+
return [], provider.name
|
|
122
|
+
|
|
123
|
+
return [], self._providers[-1].name
|
|
124
|
+
|
|
125
|
+
async def get_details(
|
|
126
|
+
self, business_id: str
|
|
127
|
+
) -> tuple[Business | None, str]:
|
|
128
|
+
"""
|
|
129
|
+
Try providers in order for a single business lookup.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
(business_or_none, provider_name)
|
|
133
|
+
"""
|
|
134
|
+
self.last_error = None
|
|
135
|
+
for provider in self._providers:
|
|
136
|
+
# Route by ID prefix when known
|
|
137
|
+
if business_id.startswith("osm_") and provider.name == "demo":
|
|
138
|
+
continue
|
|
139
|
+
if business_id.startswith("demo_") and provider.name != "demo":
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
result = await provider.get_details(business_id)
|
|
144
|
+
if result is not None:
|
|
145
|
+
return result, provider.name
|
|
146
|
+
except Exception as exc: # noqa: BLE001
|
|
147
|
+
self.last_error = str(exc)
|
|
148
|
+
logger.warning(
|
|
149
|
+
"Provider '%s' get_details failed: %s", provider.name, exc
|
|
150
|
+
)
|
|
151
|
+
return None, self._providers[-1].name
|
leadintel/server.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LeadIntel MCP Server
|
|
3
|
+
====================
|
|
4
|
+
Free, open-source MCP server for B2B lead generation and business intelligence.
|
|
5
|
+
|
|
6
|
+
Data sources:
|
|
7
|
+
- OpenStreetMap / Overpass API (free, ODbL licence)
|
|
8
|
+
- Demo data (always available, no network required)
|
|
9
|
+
|
|
10
|
+
Entry point — run with:
|
|
11
|
+
uv run python -m leadintel.server
|
|
12
|
+
or for interactive MCP Inspector:
|
|
13
|
+
uv run mcp dev src/leadintel/server.py
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
from mcp.server.mcpserver import MCPServer
|
|
21
|
+
|
|
22
|
+
from .config import settings
|
|
23
|
+
from .storage.db import init_db
|
|
24
|
+
from .tools import register_all
|
|
25
|
+
|
|
26
|
+
# ── Logging ───────────────────────────────────────────────────────────────────
|
|
27
|
+
logging.basicConfig(
|
|
28
|
+
level=getattr(logging, settings.log_level.upper(), logging.INFO),
|
|
29
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
30
|
+
)
|
|
31
|
+
logger = logging.getLogger("leadintel")
|
|
32
|
+
|
|
33
|
+
# ── Initialize Storage ────────────────────────────────────────────────────────
|
|
34
|
+
try:
|
|
35
|
+
init_db()
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
logger.warning("Failed to initialize database during startup: %s", exc)
|
|
38
|
+
|
|
39
|
+
# ── MCP Server ────────────────────────────────────────────────────────────────
|
|
40
|
+
mcp = MCPServer(
|
|
41
|
+
name="leadintel",
|
|
42
|
+
version="0.3.0",
|
|
43
|
+
instructions=(
|
|
44
|
+
"LeadIntel is a free, open-source MCP server for B2B lead generation "
|
|
45
|
+
"and business intelligence. It uses OpenStreetMap (free, no API key "
|
|
46
|
+
"required) for live business discovery. Use search_businesses to find "
|
|
47
|
+
"businesses in any city, export_leads to save results, and the "
|
|
48
|
+
"upcoming tools (analyze_website, score_lead, etc.) for enrichment. "
|
|
49
|
+
"When source is 'openstreetmap', you MUST display the attribution: "
|
|
50
|
+
"'Data © OpenStreetMap contributors, ODbL 1.0'."
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Register all tools
|
|
55
|
+
register_all(mcp)
|
|
56
|
+
|
|
57
|
+
logger.info("LeadIntel MCP server initialised. Config: %s", settings.safe_dict())
|
|
58
|
+
|
|
59
|
+
# ── Entrypoint ────────────────────────────────────────────────────────────────
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
mcp.run()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""LeadIntel modular service package."""
|
|
2
|
+
|
|
3
|
+
from .ai_business_insights import generate_ai_business_insights, generate_ai_outreach
|
|
4
|
+
from .business_comparison import compare_target_against_competitors
|
|
5
|
+
from .business_report import generate_comprehensive_business_report
|
|
6
|
+
from .competitor_research import calculate_competitor_relevance, find_industry_competitors
|
|
7
|
+
from .lead_scoring import score_business_lead
|
|
8
|
+
from .local_ai import (
|
|
9
|
+
LocalAIClient,
|
|
10
|
+
OllamaError,
|
|
11
|
+
OllamaModelNotFoundError,
|
|
12
|
+
OllamaUnavailableError,
|
|
13
|
+
local_ai_client,
|
|
14
|
+
)
|
|
15
|
+
from .robots_checker import RobotsChecker
|
|
16
|
+
from .seo_auditor import audit_html_seo, run_full_seo_audit
|
|
17
|
+
from .social_finder import extract_social_profiles
|
|
18
|
+
from .url_validator import (
|
|
19
|
+
SecurityValidationError,
|
|
20
|
+
create_safe_client,
|
|
21
|
+
validate_and_normalize_url,
|
|
22
|
+
validate_redirect_hook,
|
|
23
|
+
)
|
|
24
|
+
from .website_analyzer import analyze_target_website
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"SecurityValidationError",
|
|
28
|
+
"validate_and_normalize_url",
|
|
29
|
+
"create_safe_client",
|
|
30
|
+
"validate_redirect_hook",
|
|
31
|
+
"RobotsChecker",
|
|
32
|
+
"extract_social_profiles",
|
|
33
|
+
"audit_html_seo",
|
|
34
|
+
"run_full_seo_audit",
|
|
35
|
+
"analyze_target_website",
|
|
36
|
+
"calculate_competitor_relevance",
|
|
37
|
+
"find_industry_competitors",
|
|
38
|
+
"compare_target_against_competitors",
|
|
39
|
+
"score_business_lead",
|
|
40
|
+
"LocalAIClient",
|
|
41
|
+
"OllamaError",
|
|
42
|
+
"OllamaUnavailableError",
|
|
43
|
+
"OllamaModelNotFoundError",
|
|
44
|
+
"local_ai_client",
|
|
45
|
+
"generate_ai_business_insights",
|
|
46
|
+
"generate_ai_outreach",
|
|
47
|
+
"generate_comprehensive_business_report",
|
|
48
|
+
]
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Business Insights & Outreach Generation Service.
|
|
3
|
+
|
|
4
|
+
Bridges deterministic LeadIntel intelligence (website analysis, SEO audits, competitor benchmarks, lead scores)
|
|
5
|
+
with local Ollama inference.
|
|
6
|
+
|
|
7
|
+
Strict Ethical Rules:
|
|
8
|
+
- NEVER fabricates business revenue, marketing budgets, purchasing intent, or competitor weaknesses.
|
|
9
|
+
- Labels all AI-generated synthesis clearly as `is_ai_generated: true` while preserving verified factual sources.
|
|
10
|
+
- Grounded strictly in provided evidence: if data is absent, notes it explicitly rather than hallucinating.
|
|
11
|
+
- Generates high-impact B2B outreach across Email, LinkedIn, and WhatsApp Business.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from .local_ai import LocalAIClient, local_ai_client
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
SYSTEM_PROMPT_INSIGHTS = """You are a senior business intelligence and digital strategy analyst.
|
|
27
|
+
Your job is to provide objective, evidence-based insights for a business based EXCLUSIVELY on provided verified technical data.
|
|
28
|
+
|
|
29
|
+
STRICT CONSTRAINTS:
|
|
30
|
+
1. NEVER invent or assume company revenue, marketing budget, or customer purchasing intent.
|
|
31
|
+
2. Ground all recommendations strictly in observable digital defects and gaps (e.g. missing SSL, slow response, missing H1, poor mobile viewport, lack of social links).
|
|
32
|
+
3. Distinguish verified facts from AI strategic interpretations.
|
|
33
|
+
4. Output valid JSON when requested.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
SYSTEM_PROMPT_OUTREACH = """You are an expert B2B digital agency consultant writing personalized, respectful, and highly specific outreach messages to a business decision maker.
|
|
37
|
+
|
|
38
|
+
STRICT CONSTRAINTS & GUARDRAILS:
|
|
39
|
+
1. Do not fabricate business data or website problems. Base the outreach strictly on the provided website_opportunity_explanation.
|
|
40
|
+
2. If the site is missing (website_not_found_after_research), pitch a new build.
|
|
41
|
+
3. If a CMS is detected or a contact route is missing, mention it as a specific reason for outreach.
|
|
42
|
+
4. Do not use generic buzzwords or clickbait claims.
|
|
43
|
+
5. Propose a collaborative, value-first conversation without assuming their budget or readiness to buy.
|
|
44
|
+
6. Generate three distinct channels: Email Pitch, LinkedIn Message, and WhatsApp Business message.
|
|
45
|
+
7. Output valid JSON.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_context_summary(
|
|
50
|
+
business_name: str,
|
|
51
|
+
business_category: Optional[str] = None,
|
|
52
|
+
website_status: Optional[str] = None,
|
|
53
|
+
website_opportunity_score: Optional[float] = None,
|
|
54
|
+
website_opportunity_explanation: Optional[str] = None,
|
|
55
|
+
detected_cms: Optional[str] = None,
|
|
56
|
+
has_contact_route: Optional[bool] = None,
|
|
57
|
+
website_analysis: Optional[dict[str, Any]] = None,
|
|
58
|
+
seo_audit: Optional[dict[str, Any]] = None,
|
|
59
|
+
social_profiles: Optional[dict[str, Any]] = None,
|
|
60
|
+
competitor_comparison: Optional[dict[str, Any]] = None,
|
|
61
|
+
lead_score_data: Optional[dict[str, Any]] = None,
|
|
62
|
+
contact_info: Optional[dict[str, Any]] = None,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
"""Compile structured verified facts to ground AI synthesis."""
|
|
65
|
+
verified_facts: dict[str, Any] = {
|
|
66
|
+
"business_name": business_name,
|
|
67
|
+
"category": business_category or "Unspecified",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
website_opp: dict[str, Any] = {}
|
|
71
|
+
if website_status:
|
|
72
|
+
website_opp["website_status"] = website_status
|
|
73
|
+
if website_opportunity_score is not None:
|
|
74
|
+
website_opp["website_opportunity_score"] = website_opportunity_score
|
|
75
|
+
if website_opportunity_explanation:
|
|
76
|
+
website_opp["website_opportunity_explanation"] = website_opportunity_explanation
|
|
77
|
+
if detected_cms:
|
|
78
|
+
website_opp["detected_cms"] = detected_cms
|
|
79
|
+
if has_contact_route is not None:
|
|
80
|
+
website_opp["has_contact_route"] = has_contact_route
|
|
81
|
+
|
|
82
|
+
if website_opp:
|
|
83
|
+
verified_facts["website_opportunity_and_tech"] = website_opp
|
|
84
|
+
|
|
85
|
+
if contact_info:
|
|
86
|
+
verified_facts["contact"] = {
|
|
87
|
+
"city": contact_info.get("city"),
|
|
88
|
+
"has_phone": bool(contact_info.get("phone")),
|
|
89
|
+
"has_email": bool(contact_info.get("email")),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if website_analysis:
|
|
93
|
+
verified_facts["website"] = {
|
|
94
|
+
"url": website_analysis.get("url"),
|
|
95
|
+
"available": website_analysis.get("available"),
|
|
96
|
+
"is_https": website_analysis.get("protocol", {}).get("is_https"),
|
|
97
|
+
"response_time_seconds": website_analysis.get("response_time_seconds"),
|
|
98
|
+
"mobile_viewport_configured": website_analysis.get("metadata", {}).get("has_mobile_viewport"),
|
|
99
|
+
"has_contact_form": website_analysis.get("contact_detection", {}).get("has_contact_form"),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if seo_audit:
|
|
103
|
+
verified_facts["seo"] = {
|
|
104
|
+
"score": seo_audit.get("seo_score"),
|
|
105
|
+
"issues_sample": [iss.get("message") for iss in seo_audit.get("issues", [])][:4],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if social_profiles:
|
|
109
|
+
profiles_dict = social_profiles.get("profiles", social_profiles)
|
|
110
|
+
active = [plat for plat, links in profiles_dict.items() if isinstance(links, list) and len(links) > 0]
|
|
111
|
+
verified_facts["social_media"] = {
|
|
112
|
+
"active_channels": active,
|
|
113
|
+
"channel_count": len(active),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if competitor_comparison:
|
|
117
|
+
verified_facts["competitor_context"] = {
|
|
118
|
+
"competitors_compared": competitor_comparison.get("competitors_compared_count", 0),
|
|
119
|
+
"strengths_sample": competitor_comparison.get("strengths", [])[:2],
|
|
120
|
+
"weaknesses_sample": competitor_comparison.get("weaknesses", [])[:2],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if lead_score_data:
|
|
124
|
+
verified_facts["lead_score"] = {
|
|
125
|
+
"overall_score": lead_score_data.get("overall_lead_score"),
|
|
126
|
+
"priority": lead_score_data.get("priority_classification"),
|
|
127
|
+
"service_opportunities": lead_score_data.get("service_opportunities"),
|
|
128
|
+
"recommended_package": lead_score_data.get("recommended_service_package"),
|
|
129
|
+
"verified_issues": lead_score_data.get("verified_issues", [])[:5],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return verified_facts
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def generate_ai_business_insights(
|
|
136
|
+
business_name: str,
|
|
137
|
+
business_category: Optional[str] = None,
|
|
138
|
+
website_analysis: Optional[dict[str, Any]] = None,
|
|
139
|
+
seo_audit: Optional[dict[str, Any]] = None,
|
|
140
|
+
social_profiles: Optional[dict[str, Any]] = None,
|
|
141
|
+
competitor_comparison: Optional[dict[str, Any]] = None,
|
|
142
|
+
lead_score_data: Optional[dict[str, Any]] = None,
|
|
143
|
+
contact_info: Optional[dict[str, Any]] = None,
|
|
144
|
+
ai_client: Optional[LocalAIClient] = None,
|
|
145
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
"""
|
|
148
|
+
Generate evidence-based strategic business insights using local Ollama.
|
|
149
|
+
"""
|
|
150
|
+
if not business_name or not business_name.strip():
|
|
151
|
+
raise ValueError("business_name must be a non-empty string.")
|
|
152
|
+
|
|
153
|
+
client = ai_client or local_ai_client
|
|
154
|
+
verified_data = _build_context_summary(
|
|
155
|
+
business_name=business_name,
|
|
156
|
+
business_category=business_category,
|
|
157
|
+
website_analysis=website_analysis,
|
|
158
|
+
seo_audit=seo_audit,
|
|
159
|
+
social_profiles=social_profiles,
|
|
160
|
+
competitor_comparison=competitor_comparison,
|
|
161
|
+
lead_score_data=lead_score_data,
|
|
162
|
+
contact_info=contact_info,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
prompt = f"""Analyze the verified business data below and produce a structured strategic insight report in JSON format.
|
|
166
|
+
|
|
167
|
+
VERIFIED DATA:
|
|
168
|
+
{json.dumps(verified_data, indent=2)}
|
|
169
|
+
|
|
170
|
+
Required JSON schema:
|
|
171
|
+
{{
|
|
172
|
+
"executive_summary": "string summarizing digital maturity and key findings",
|
|
173
|
+
"opportunity_explanation": "string explaining why specific digital opportunities exist based on the evidence",
|
|
174
|
+
"lead_score_interpretation": "string contextualizing the deterministic lead score without altering it",
|
|
175
|
+
"competitor_insights": "string comparing their posture against market peers",
|
|
176
|
+
"service_recommendations": [
|
|
177
|
+
{{
|
|
178
|
+
"service": "Website Development | SEO | Social Media Management | Digital Marketing",
|
|
179
|
+
"rationale": "specific evidence-backed reason",
|
|
180
|
+
"priority": "High | Medium | Low"
|
|
181
|
+
}}
|
|
182
|
+
]
|
|
183
|
+
}}
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
raw_response = await client.generate(
|
|
187
|
+
prompt=prompt,
|
|
188
|
+
system_prompt=SYSTEM_PROMPT_INSIGHTS,
|
|
189
|
+
format_json=True,
|
|
190
|
+
client=http_client,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
parsed_insights = json.loads(raw_response)
|
|
195
|
+
except json.JSONDecodeError:
|
|
196
|
+
parsed_insights = {
|
|
197
|
+
"executive_summary": raw_response,
|
|
198
|
+
"opportunity_explanation": "Extracted from local AI response.",
|
|
199
|
+
"lead_score_interpretation": "See deterministic lead score report.",
|
|
200
|
+
"competitor_insights": "Refer to competitor comparison matrix.",
|
|
201
|
+
"service_recommendations": [],
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"business_name": business_name,
|
|
206
|
+
"is_ai_generated": True,
|
|
207
|
+
"ai_model_used": client.model,
|
|
208
|
+
"verified_facts_grounding": verified_data,
|
|
209
|
+
"ai_insights": parsed_insights,
|
|
210
|
+
"disclaimer": (
|
|
211
|
+
"Observations under 'ai_insights' were synthesized by a local AI model grounded strictly in verified data. "
|
|
212
|
+
"No assumptions regarding customer budget or revenue were made."
|
|
213
|
+
),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def generate_ai_outreach(
|
|
218
|
+
business_name: str,
|
|
219
|
+
business_category: Optional[str] = None,
|
|
220
|
+
website_status: Optional[str] = None,
|
|
221
|
+
website_opportunity_score: Optional[float] = None,
|
|
222
|
+
website_opportunity_explanation: Optional[str] = None,
|
|
223
|
+
detected_cms: Optional[str] = None,
|
|
224
|
+
has_contact_route: Optional[bool] = None,
|
|
225
|
+
website_analysis: Optional[dict[str, Any]] = None,
|
|
226
|
+
seo_audit: Optional[dict[str, Any]] = None,
|
|
227
|
+
social_profiles: Optional[dict[str, Any]] = None,
|
|
228
|
+
competitor_comparison: Optional[dict[str, Any]] = None,
|
|
229
|
+
lead_score_data: Optional[dict[str, Any]] = None,
|
|
230
|
+
contact_info: Optional[dict[str, Any]] = None,
|
|
231
|
+
agency_name: str = "Our Agency",
|
|
232
|
+
sender_name: str = "Lead Specialist",
|
|
233
|
+
ai_client: Optional[LocalAIClient] = None,
|
|
234
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
"""
|
|
237
|
+
Generate tailored B2B outreach messages across Email, LinkedIn, and WhatsApp Business.
|
|
238
|
+
"""
|
|
239
|
+
if not business_name or not business_name.strip():
|
|
240
|
+
raise ValueError("business_name must be a non-empty string.")
|
|
241
|
+
|
|
242
|
+
client = ai_client or local_ai_client
|
|
243
|
+
verified_data = _build_context_summary(
|
|
244
|
+
business_name=business_name,
|
|
245
|
+
business_category=business_category,
|
|
246
|
+
website_status=website_status,
|
|
247
|
+
website_opportunity_score=website_opportunity_score,
|
|
248
|
+
website_opportunity_explanation=website_opportunity_explanation,
|
|
249
|
+
detected_cms=detected_cms,
|
|
250
|
+
has_contact_route=has_contact_route,
|
|
251
|
+
website_analysis=website_analysis,
|
|
252
|
+
seo_audit=seo_audit,
|
|
253
|
+
social_profiles=social_profiles,
|
|
254
|
+
competitor_comparison=competitor_comparison,
|
|
255
|
+
lead_score_data=lead_score_data,
|
|
256
|
+
contact_info=contact_info,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
prompt = f"""Generate personalized, professional B2B outreach messages for {business_name} in JSON format.
|
|
260
|
+
Sender: {sender_name} representing {agency_name}.
|
|
261
|
+
|
|
262
|
+
STRICT GUARDRAILS:
|
|
263
|
+
- Do not fabricate business data or website problems. Base the outreach strictly on the provided website_opportunity_explanation.
|
|
264
|
+
- If the site is missing (website_not_found_after_research), pitch a new build.
|
|
265
|
+
- If a CMS is detected or a contact route is missing, mention it as a specific reason for outreach.
|
|
266
|
+
|
|
267
|
+
VERIFIED CONTEXT:
|
|
268
|
+
{json.dumps(verified_data, indent=2)}
|
|
269
|
+
|
|
270
|
+
Required JSON schema:
|
|
271
|
+
{{
|
|
272
|
+
"email_pitch": {{
|
|
273
|
+
"subject": "Clear, curiosity-inducing subject line",
|
|
274
|
+
"body": "Professional 3-paragraph email referencing 1-2 verified technical findings and a low-friction call-to-action."
|
|
275
|
+
}},
|
|
276
|
+
"linkedin_message": {{
|
|
277
|
+
"message": "Concise connection message (under 300 characters) referencing their business and specific digital insight."
|
|
278
|
+
}},
|
|
279
|
+
"whatsapp_message": {{
|
|
280
|
+
"message": "Polite, direct WhatsApp Business message (under 120 words) with clear value proposition and polite sign-off."
|
|
281
|
+
}}
|
|
282
|
+
}}
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
raw_response = await client.generate(
|
|
286
|
+
prompt=prompt,
|
|
287
|
+
system_prompt=SYSTEM_PROMPT_OUTREACH,
|
|
288
|
+
format_json=True,
|
|
289
|
+
client=http_client,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
parsed_outreach = json.loads(raw_response)
|
|
294
|
+
except json.JSONDecodeError:
|
|
295
|
+
parsed_outreach = {
|
|
296
|
+
"email_pitch": {"subject": f"Quick thought regarding {business_name}", "body": raw_response},
|
|
297
|
+
"linkedin_message": {"message": raw_response[:280]},
|
|
298
|
+
"whatsapp_message": {"message": raw_response[:300]},
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
"business_name": business_name,
|
|
303
|
+
"is_ai_generated": True,
|
|
304
|
+
"ai_model_used": client.model,
|
|
305
|
+
"outreach_campaign": parsed_outreach,
|
|
306
|
+
"disclaimer": (
|
|
307
|
+
"These outreach drafts were synthesized by local AI using verified evidence. "
|
|
308
|
+
"Review and adapt messages before sending to prospects."
|
|
309
|
+
),
|
|
310
|
+
}
|