leadgen-ai 1.2.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.
agent/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """LeadGen AI — keyword lead search with any AI API. Developed by Gyan Ranjan."""
2
+
3
+ from .pipeline import LeadAgent
4
+ from .branding import APP_NAME, CREDIT, DEVELOPER, VERSION
5
+
6
+ __all__ = ["LeadAgent", "APP_NAME", "CREDIT", "DEVELOPER", "VERSION"]
agent/branding.py ADDED
@@ -0,0 +1,15 @@
1
+ """Product branding — LeadGen AI by Gyan Ranjan."""
2
+
3
+ APP_NAME = "LeadGen AI"
4
+ APP_TAGLINE = "Search any keywords → get real sales leads (any AI API)"
5
+ DEVELOPER = "Gyan Ranjan"
6
+ CREDIT = f"Developed by {DEVELOPER}"
7
+ VERSION = "1.2.0"
8
+
9
+
10
+ def banner_lines() -> str:
11
+ return (
12
+ f"[bold cyan]{APP_NAME}[/] v{VERSION}\n"
13
+ f"[dim]{APP_TAGLINE}[/]\n"
14
+ f"[magenta]{CREDIT}[/]"
15
+ )
agent/config.py ADDED
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ from .models_catalog import FAMOUS_MODELS, PROVIDERS
10
+
11
+ _ROOT = Path(__file__).resolve().parent.parent
12
+ load_dotenv(_ROOT / ".env")
13
+ load_dotenv()
14
+
15
+
16
+ def _first_env(*names: str) -> str:
17
+ for name in names:
18
+ val = os.getenv(name, "").strip()
19
+ if val:
20
+ return val
21
+ return ""
22
+
23
+
24
+ def resolve_api_key(provider: str) -> str:
25
+ """Accept any common API key env var so users can plug any provider."""
26
+ meta = PROVIDERS.get(provider, PROVIDERS["custom"])
27
+ # provider-specific first, then generic aliases
28
+ return _first_env(
29
+ meta["key_env"],
30
+ "LEADGEN_API_KEY",
31
+ "API_KEY",
32
+ "OPENAI_API_KEY",
33
+ "XAI_API_KEY",
34
+ "GROQ_API_KEY",
35
+ "ANTHROPIC_API_KEY",
36
+ "GEMINI_API_KEY",
37
+ "GOOGLE_API_KEY",
38
+ "DEEPSEEK_API_KEY",
39
+ "OPENROUTER_API_KEY",
40
+ "TOGETHER_API_KEY",
41
+ "MISTRAL_API_KEY",
42
+ )
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class Settings:
47
+ api_key: str
48
+ base_url: str
49
+ model: str
50
+ provider: str = "openai"
51
+ default_lead_count: int = 10
52
+ min_score: int = 50
53
+ use_web_search: bool = True
54
+
55
+ @classmethod
56
+ def from_env(
57
+ cls,
58
+ *,
59
+ provider: str | None = None,
60
+ model: str | None = None,
61
+ base_url: str | None = None,
62
+ api_key: str | None = None,
63
+ ) -> "Settings":
64
+ # CLI / env overrides
65
+ model_name = (model or os.getenv("LEADGEN_MODEL") or os.getenv("MODEL") or "").strip()
66
+ provider_name = (provider or os.getenv("LEADGEN_PROVIDER") or os.getenv("PROVIDER") or "").strip().lower()
67
+
68
+ # Resolve famous model shortcuts → provider + model id
69
+ if model_name and model_name.lower() in FAMOUS_MODELS:
70
+ auto_provider, auto_model = FAMOUS_MODELS[model_name.lower()]
71
+ if not provider_name:
72
+ provider_name = auto_provider
73
+ model_name = auto_model
74
+
75
+ if not provider_name:
76
+ # Auto-detect from which key is set
77
+ for p, meta in PROVIDERS.items():
78
+ if p == "custom":
79
+ continue
80
+ if os.getenv(meta["key_env"], "").strip():
81
+ provider_name = p
82
+ break
83
+ if not provider_name:
84
+ provider_name = "openai"
85
+
86
+ if provider_name not in PROVIDERS:
87
+ provider_name = "custom"
88
+
89
+ meta = PROVIDERS[provider_name]
90
+ resolved_base = (
91
+ (base_url or "").strip()
92
+ or os.getenv("LEADGEN_BASE_URL", "").strip()
93
+ or os.getenv("OPENAI_BASE_URL", "").strip()
94
+ or meta["base_url"]
95
+ )
96
+ if not resolved_base:
97
+ resolved_base = "https://api.openai.com/v1"
98
+
99
+ if not model_name:
100
+ model_name = (
101
+ os.getenv("LEADGEN_MODEL", "").strip()
102
+ or meta["default_model"]
103
+ )
104
+
105
+ key = (api_key or "").strip() or resolve_api_key(provider_name)
106
+ if not key:
107
+ raise RuntimeError(
108
+ "Missing API key. Set any of these in .env or environment:\n"
109
+ " OPENAI_API_KEY, GROQ_API_KEY, GEMINI_API_KEY, DEEPSEEK_API_KEY,\n"
110
+ " OPENROUTER_API_KEY, MISTRAL_API_KEY, XAI_API_KEY, LEADGEN_API_KEY, API_KEY\n\n"
111
+ "Examples:\n"
112
+ " set OPENAI_API_KEY=sk-...\n"
113
+ " leadgen search \"dental clinics Mumbai\" --model gpt-4o\n"
114
+ " leadgen search \"gyms Delhi\" --provider groq --model llama-3.3-70b\n"
115
+ " leadgen models # list popular models\n"
116
+ )
117
+
118
+ web = os.getenv("LEADGEN_WEB_SEARCH", "1").strip().lower() not in {
119
+ "0",
120
+ "false",
121
+ "no",
122
+ "off",
123
+ }
124
+
125
+ return cls(
126
+ api_key=key,
127
+ base_url=resolved_base.rstrip("/"),
128
+ model=model_name,
129
+ provider=provider_name,
130
+ default_lead_count=int(os.getenv("DEFAULT_LEAD_COUNT", "10")),
131
+ min_score=int(os.getenv("MIN_LEAD_SCORE", "50")),
132
+ use_web_search=web,
133
+ )
agent/export.py ADDED
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import json
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Iterable
8
+
9
+ from .models import Lead, LeadRunResult
10
+
11
+ CSV_FIELDS = [
12
+ "company_name",
13
+ "website",
14
+ "industry",
15
+ "company_size",
16
+ "location",
17
+ "description",
18
+ "contact_name",
19
+ "contact_title",
20
+ "contact_email",
21
+ "linkedin_url",
22
+ "source_url",
23
+ "buying_signals",
24
+ "fit_score",
25
+ "fit_reason",
26
+ "outreach_subject",
27
+ "outreach_email",
28
+ "next_action",
29
+ ]
30
+
31
+
32
+ def default_output_dir() -> Path:
33
+ root = Path(__file__).resolve().parent.parent / "output"
34
+ root.mkdir(parents=True, exist_ok=True)
35
+ return root
36
+
37
+
38
+ def stamp() -> str:
39
+ return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
40
+
41
+
42
+ def _lead_to_row(lead: Lead) -> dict:
43
+ row = lead.model_dump()
44
+ signals = row.get("buying_signals") or []
45
+ if isinstance(signals, list):
46
+ row["buying_signals"] = "; ".join(str(s) for s in signals)
47
+ return row
48
+
49
+
50
+ def export_csv(leads: list[Lead], path: Path) -> Path:
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ with path.open("w", newline="", encoding="utf-8") as f:
53
+ writer = csv.DictWriter(f, fieldnames=CSV_FIELDS, extrasaction="ignore")
54
+ writer.writeheader()
55
+ for lead in leads:
56
+ writer.writerow(_lead_to_row(lead))
57
+ return path
58
+
59
+
60
+ def export_json(result: LeadRunResult, path: Path) -> Path:
61
+ path.parent.mkdir(parents=True, exist_ok=True)
62
+ path.write_text(result.model_dump_json(indent=2), encoding="utf-8")
63
+ return path
64
+
65
+
66
+ def _normalize_key(name: str, website: str | None = None) -> str:
67
+ base = (name or "").strip().lower()
68
+ site = (website or "").strip().lower().removeprefix("https://").removeprefix("http://").removeprefix("www.")
69
+ site = site.rstrip("/")
70
+ return f"{base}|{site}"
71
+
72
+
73
+ def load_csv_rows(path: Path) -> list[dict]:
74
+ if not path.is_file():
75
+ return []
76
+ with path.open(newline="", encoding="utf-8") as f:
77
+ return list(csv.DictReader(f))
78
+
79
+
80
+ def merge_lead_rows(rows: Iterable[dict]) -> list[dict]:
81
+ """Dedupe by company+website, keep highest fit_score."""
82
+ best: dict[str, dict] = {}
83
+ for row in rows:
84
+ name = (row.get("company_name") or "").strip()
85
+ if not name:
86
+ continue
87
+ key = _normalize_key(name, row.get("website"))
88
+ try:
89
+ score = int(float(row.get("fit_score") or 0))
90
+ except (TypeError, ValueError):
91
+ score = 0
92
+ row = {**row, "fit_score": score}
93
+ prev = best.get(key)
94
+ if prev is None:
95
+ best[key] = row
96
+ continue
97
+ try:
98
+ prev_score = int(float(prev.get("fit_score") or 0))
99
+ except (TypeError, ValueError):
100
+ prev_score = 0
101
+ if score >= prev_score:
102
+ best[key] = row
103
+ merged = list(best.values())
104
+ merged.sort(key=lambda r: int(r.get("fit_score") or 0), reverse=True)
105
+ return merged
106
+
107
+
108
+ def write_rows_csv(rows: list[dict], path: Path) -> Path:
109
+ path.parent.mkdir(parents=True, exist_ok=True)
110
+ with path.open("w", newline="", encoding="utf-8") as f:
111
+ writer = csv.DictWriter(f, fieldnames=CSV_FIELDS, extrasaction="ignore")
112
+ writer.writeheader()
113
+ for row in rows:
114
+ out = {k: row.get(k, "") for k in CSV_FIELDS}
115
+ writer.writerow(out)
116
+ return path
117
+
118
+
119
+ def merge_all_csvs(out_dir: Path | None = None) -> Path:
120
+ """Merge every leads_*.csv in output/ into all_leads.csv (deduped)."""
121
+ out = out_dir or default_output_dir()
122
+ rows: list[dict] = []
123
+ for path in sorted(out.glob("leads_*.csv")):
124
+ rows.extend(load_csv_rows(path))
125
+ # Also include existing master if present
126
+ master = out / "all_leads.csv"
127
+ if master.is_file():
128
+ rows.extend(load_csv_rows(master))
129
+ merged = merge_lead_rows(rows)
130
+ return write_rows_csv(merged, master)
131
+
132
+
133
+ def append_to_master(leads: list[Lead], out_dir: Path | None = None) -> Path:
134
+ out = out_dir or default_output_dir()
135
+ master = out / "all_leads.csv"
136
+ existing = load_csv_rows(master)
137
+ new_rows = [_lead_to_row(lead) for lead in leads]
138
+ merged = merge_lead_rows(existing + new_rows)
139
+ return write_rows_csv(merged, master)
140
+
141
+
142
+ def export_all(result: LeadRunResult, out_dir: Path | None = None) -> dict[str, Path]:
143
+ out = out_dir or default_output_dir()
144
+ ts = stamp()
145
+ csv_path = export_csv(result.leads, out / f"leads_{ts}.csv")
146
+ json_path = export_json(result, out / f"leads_{ts}.json")
147
+ master_path = append_to_master(result.leads, out)
148
+ return {"csv": csv_path, "json": json_path, "all": master_path}
agent/llm.py ADDED
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any, Type, TypeVar
6
+
7
+ from openai import OpenAI
8
+ from pydantic import BaseModel
9
+
10
+ from .config import Settings
11
+
12
+ T = TypeVar("T", bound=BaseModel)
13
+
14
+
15
+ class LLM:
16
+ """OpenAI-compatible client — works with OpenAI, Groq, Gemini, DeepSeek, OpenRouter, xAI, custom."""
17
+
18
+ def __init__(self, settings: Settings):
19
+ self.settings = settings
20
+ self.client = OpenAI(
21
+ api_key=settings.api_key,
22
+ base_url=settings.base_url,
23
+ )
24
+
25
+ def complete(
26
+ self,
27
+ system: str,
28
+ user: str,
29
+ *,
30
+ use_web_search: bool = False,
31
+ temperature: float = 0.3,
32
+ ) -> str:
33
+ # use_web_search is handled upstream via agent/search.py for all providers
34
+ _ = use_web_search
35
+ kwargs: dict[str, Any] = {
36
+ "model": self.settings.model,
37
+ "messages": [
38
+ {"role": "system", "content": system},
39
+ {"role": "user", "content": user},
40
+ ],
41
+ "temperature": temperature,
42
+ }
43
+ # Some reasoning models reject temperature
44
+ try:
45
+ response = self.client.chat.completions.create(**kwargs)
46
+ except Exception:
47
+ kwargs.pop("temperature", None)
48
+ response = self.client.chat.completions.create(**kwargs)
49
+
50
+ choice = response.choices[0].message
51
+ content = choice.content
52
+ if isinstance(content, str) and content.strip():
53
+ return content.strip()
54
+ return str(content or "").strip()
55
+
56
+ def complete_json(
57
+ self,
58
+ system: str,
59
+ user: str,
60
+ schema: Type[T],
61
+ *,
62
+ use_web_search: bool = False,
63
+ temperature: float = 0.2,
64
+ ) -> T:
65
+ schema_hint = json.dumps(schema.model_json_schema(), indent=2)
66
+ full_system = (
67
+ f"{system}\n\n"
68
+ "Return ONLY valid JSON matching this JSON Schema. "
69
+ "No markdown fences, no commentary.\n\n"
70
+ f"{schema_hint}"
71
+ )
72
+ raw = self.complete(
73
+ full_system,
74
+ user,
75
+ use_web_search=use_web_search,
76
+ temperature=temperature,
77
+ )
78
+ data = _parse_json(raw)
79
+ return schema.model_validate(data)
80
+
81
+
82
+ def _parse_json(text: str) -> Any:
83
+ text = text.strip()
84
+ fence = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
85
+ if fence:
86
+ text = fence.group(1).strip()
87
+ try:
88
+ return json.loads(text)
89
+ except json.JSONDecodeError:
90
+ for pattern in (r"\{[\s\S]*\}", r"\[[\s\S]*\]"):
91
+ match = re.search(pattern, text)
92
+ if match:
93
+ return json.loads(match.group(0))
94
+ raise
agent/models.py ADDED
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class IdealCustomerProfile(BaseModel):
9
+ """Structured ideal customer profile derived from the user's product pitch."""
10
+
11
+ product_summary: str = Field(description="What you sell, in 1-2 sentences")
12
+ target_industries: list[str] = Field(default_factory=list)
13
+ company_sizes: list[str] = Field(
14
+ default_factory=list,
15
+ description="e.g. startup, SMB, mid-market, enterprise",
16
+ )
17
+ geographies: list[str] = Field(default_factory=list)
18
+ buyer_titles: list[str] = Field(
19
+ default_factory=list,
20
+ description="Decision-maker roles to target",
21
+ )
22
+ pain_points: list[str] = Field(default_factory=list)
23
+ buying_signals: list[str] = Field(
24
+ default_factory=list,
25
+ description="Public signals that indicate a good fit",
26
+ )
27
+ disqualifiers: list[str] = Field(default_factory=list)
28
+ search_queries: list[str] = Field(
29
+ default_factory=list,
30
+ description="Web search queries to find matching companies",
31
+ )
32
+ value_proposition: str = ""
33
+
34
+
35
+ class Lead(BaseModel):
36
+ company_name: str
37
+ website: Optional[str] = None
38
+ industry: Optional[str] = None
39
+ company_size: Optional[str] = None
40
+ location: Optional[str] = None
41
+ description: Optional[str] = None
42
+ contact_name: Optional[str] = None
43
+ contact_title: Optional[str] = None
44
+ contact_email: Optional[str] = None
45
+ linkedin_url: Optional[str] = None
46
+ source_url: Optional[str] = None
47
+ buying_signals: list[str] = Field(default_factory=list)
48
+ fit_score: int = Field(ge=0, le=100, default=0)
49
+ fit_reason: str = ""
50
+ outreach_subject: str = ""
51
+ outreach_email: str = ""
52
+ next_action: str = ""
53
+
54
+
55
+ class LeadBatch(BaseModel):
56
+ leads: list[Lead] = Field(default_factory=list)
57
+ research_notes: str = ""
58
+
59
+
60
+ class LeadRunResult(BaseModel):
61
+ icp: IdealCustomerProfile
62
+ leads: list[Lead]
63
+ research_notes: str = ""
64
+ query: str = ""
@@ -0,0 +1,97 @@
1
+ """Popular models users can pick with --model or LEADGEN_MODEL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # name -> (base_url, default_api_key_env, notes)
6
+ PROVIDERS: dict[str, dict[str, str]] = {
7
+ "openai": {
8
+ "base_url": "https://api.openai.com/v1",
9
+ "key_env": "OPENAI_API_KEY",
10
+ "default_model": "gpt-4o",
11
+ },
12
+ "xai": {
13
+ "base_url": "https://api.x.ai/v1",
14
+ "key_env": "XAI_API_KEY",
15
+ "default_model": "grok-3",
16
+ },
17
+ "groq": {
18
+ "base_url": "https://api.groq.com/openai/v1",
19
+ "key_env": "GROQ_API_KEY",
20
+ "default_model": "llama-3.3-70b-versatile",
21
+ },
22
+ "deepseek": {
23
+ "base_url": "https://api.deepseek.com/v1",
24
+ "key_env": "DEEPSEEK_API_KEY",
25
+ "default_model": "deepseek-chat",
26
+ },
27
+ "openrouter": {
28
+ "base_url": "https://openrouter.ai/api/v1",
29
+ "key_env": "OPENROUTER_API_KEY",
30
+ "default_model": "openai/gpt-4o-mini",
31
+ },
32
+ "together": {
33
+ "base_url": "https://api.together.xyz/v1",
34
+ "key_env": "TOGETHER_API_KEY",
35
+ "default_model": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
36
+ },
37
+ "mistral": {
38
+ "base_url": "https://api.mistral.ai/v1",
39
+ "key_env": "MISTRAL_API_KEY",
40
+ "default_model": "mistral-large-latest",
41
+ },
42
+ "gemini": {
43
+ "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/",
44
+ "key_env": "GEMINI_API_KEY",
45
+ "default_model": "gemini-2.0-flash",
46
+ },
47
+ "custom": {
48
+ "base_url": "", # set via LEADGEN_BASE_URL
49
+ "key_env": "LEADGEN_API_KEY",
50
+ "default_model": "gpt-4o",
51
+ },
52
+ }
53
+
54
+ # Famous models shortcut → (provider, model_id)
55
+ FAMOUS_MODELS: dict[str, tuple[str, str]] = {
56
+ # OpenAI
57
+ "gpt-4o": ("openai", "gpt-4o"),
58
+ "gpt-4o-mini": ("openai", "gpt-4o-mini"),
59
+ "gpt-4.1": ("openai", "gpt-4.1"),
60
+ "gpt-4.1-mini": ("openai", "gpt-4.1-mini"),
61
+ "o3-mini": ("openai", "o3-mini"),
62
+ "o4-mini": ("openai", "o4-mini"),
63
+ # Google
64
+ "gemini-2.0-flash": ("gemini", "gemini-2.0-flash"),
65
+ "gemini-1.5-pro": ("gemini", "gemini-1.5-pro"),
66
+ "gemini-2.5-pro": ("gemini", "gemini-2.5-pro"),
67
+ # Meta via Groq
68
+ "llama-3.3-70b": ("groq", "llama-3.3-70b-versatile"),
69
+ "llama-3.1-70b": ("groq", "llama-3.3-70b-versatile"),
70
+ "mixtral-8x7b": ("groq", "mixtral-8x7b-32768"),
71
+ # DeepSeek
72
+ "deepseek-chat": ("deepseek", "deepseek-chat"),
73
+ "deepseek-v3": ("deepseek", "deepseek-chat"),
74
+ # Mistral
75
+ "mistral-large": ("mistral", "mistral-large-latest"),
76
+ "mistral-small": ("mistral", "mistral-small-latest"),
77
+ # xAI (optional)
78
+ "grok-3": ("xai", "grok-3"),
79
+ "grok-4": ("xai", "grok-4"),
80
+ "grok-4.5": ("xai", "grok-4.5"),
81
+ # OpenRouter popular
82
+ "claude-3.5-sonnet": ("openrouter", "anthropic/claude-3.5-sonnet"),
83
+ "claude-sonnet-4": ("openrouter", "anthropic/claude-sonnet-4"),
84
+ "claude-3-haiku": ("openrouter", "anthropic/claude-3-haiku"),
85
+ }
86
+
87
+
88
+ def list_models_help() -> str:
89
+ lines = ["Popular models (use --model NAME):"]
90
+ for name in sorted(FAMOUS_MODELS.keys()):
91
+ provider, mid = FAMOUS_MODELS[name]
92
+ lines.append(f" {name:22} → {provider:12} {mid}")
93
+ lines.append("")
94
+ lines.append("Providers (use --provider NAME + your API key):")
95
+ for p, meta in PROVIDERS.items():
96
+ lines.append(f" {p:12} key={meta['key_env']} default={meta['default_model']}")
97
+ return "\n".join(lines)