leadgen-ai 1.2.0__tar.gz
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.
- leadgen_ai-1.2.0/.env.example +30 -0
- leadgen_ai-1.2.0/LICENSE +21 -0
- leadgen_ai-1.2.0/MANIFEST.in +14 -0
- leadgen_ai-1.2.0/PKG-INFO +158 -0
- leadgen_ai-1.2.0/README.md +129 -0
- leadgen_ai-1.2.0/agent/__init__.py +6 -0
- leadgen_ai-1.2.0/agent/branding.py +15 -0
- leadgen_ai-1.2.0/agent/config.py +133 -0
- leadgen_ai-1.2.0/agent/export.py +148 -0
- leadgen_ai-1.2.0/agent/llm.py +94 -0
- leadgen_ai-1.2.0/agent/models.py +64 -0
- leadgen_ai-1.2.0/agent/models_catalog.py +97 -0
- leadgen_ai-1.2.0/agent/pipeline.py +330 -0
- leadgen_ai-1.2.0/agent/search.py +67 -0
- leadgen_ai-1.2.0/install.ps1 +123 -0
- leadgen_ai-1.2.0/leadgen.cmd +31 -0
- leadgen_ai-1.2.0/leadgen_ai.egg-info/PKG-INFO +158 -0
- leadgen_ai-1.2.0/leadgen_ai.egg-info/SOURCES.txt +26 -0
- leadgen_ai-1.2.0/leadgen_ai.egg-info/dependency_links.txt +1 -0
- leadgen_ai-1.2.0/leadgen_ai.egg-info/entry_points.txt +3 -0
- leadgen_ai-1.2.0/leadgen_ai.egg-info/requires.txt +6 -0
- leadgen_ai-1.2.0/leadgen_ai.egg-info/top_level.txt +2 -0
- leadgen_ai-1.2.0/main.py +455 -0
- leadgen_ai-1.2.0/pyproject.toml +49 -0
- leadgen_ai-1.2.0/requirements.txt +6 -0
- leadgen_ai-1.2.0/run.bat +9 -0
- leadgen_ai-1.2.0/run.ps1 +84 -0
- leadgen_ai-1.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# LeadGen AI — use ANY OpenAI-compatible API
|
|
2
|
+
# Developed by Gyan Ranjan
|
|
3
|
+
|
|
4
|
+
# --- Pick ONE key (whichever provider you use) ---
|
|
5
|
+
OPENAI_API_KEY=
|
|
6
|
+
# GROQ_API_KEY=
|
|
7
|
+
# GEMINI_API_KEY=
|
|
8
|
+
# DEEPSEEK_API_KEY=
|
|
9
|
+
# OPENROUTER_API_KEY=
|
|
10
|
+
# MISTRAL_API_KEY=
|
|
11
|
+
# TOGETHER_API_KEY=
|
|
12
|
+
# XAI_API_KEY=
|
|
13
|
+
# LEADGEN_API_KEY= # generic alias
|
|
14
|
+
# API_KEY= # generic alias
|
|
15
|
+
|
|
16
|
+
# --- Optional: force provider / model ---
|
|
17
|
+
# LEADGEN_PROVIDER=openai
|
|
18
|
+
# LEADGEN_MODEL=gpt-4o
|
|
19
|
+
# Famous shortcuts: gpt-4o, gpt-4o-mini, gemini-2.0-flash, llama-3.3-70b,
|
|
20
|
+
# deepseek-chat, mistral-large, claude-3.5-sonnet, o3-mini
|
|
21
|
+
|
|
22
|
+
# --- Custom OpenAI-compatible endpoint (Ollama, vLLM, Azure, etc.) ---
|
|
23
|
+
# LEADGEN_PROVIDER=custom
|
|
24
|
+
# LEADGEN_BASE_URL=http://localhost:11434/v1
|
|
25
|
+
# LEADGEN_MODEL=llama3.2
|
|
26
|
+
# LEADGEN_API_KEY=ollama
|
|
27
|
+
|
|
28
|
+
# LEADGEN_WEB_SEARCH=1
|
|
29
|
+
# DEFAULT_LEAD_COUNT=10
|
|
30
|
+
# MIN_LEAD_SCORE=40
|
leadgen_ai-1.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gyan Ranjan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include requirements.txt
|
|
4
|
+
include .env.example
|
|
5
|
+
include leadgen.cmd
|
|
6
|
+
include run.ps1
|
|
7
|
+
include run.bat
|
|
8
|
+
include install.ps1
|
|
9
|
+
recursive-include agent *.py
|
|
10
|
+
prune output
|
|
11
|
+
prune .venv
|
|
12
|
+
global-exclude __pycache__
|
|
13
|
+
global-exclude *.py[cod]
|
|
14
|
+
global-exclude .env
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leadgen-ai
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Search any keywords and generate real sales leads. Works with any AI API (OpenAI, Groq, Gemini, DeepSeek, OpenRouter, …). Developed by Gyan Ranjan.
|
|
5
|
+
Author: Gyan Ranjan
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Gyan4589/leadgen-ai
|
|
8
|
+
Project-URL: Repository, https://github.com/Gyan4589/leadgen-ai
|
|
9
|
+
Project-URL: Issues, https://github.com/Gyan4589/leadgen-ai/issues
|
|
10
|
+
Keywords: leads,sales,cli,ai,lead-generation,openai,gpt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Office/Business
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: openai>=1.60.0
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: rich>=13.0.0
|
|
26
|
+
Requires-Dist: typer>=0.12.0
|
|
27
|
+
Requires-Dist: ddgs>=9.0.0
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# LeadGen AI
|
|
31
|
+
|
|
32
|
+
**Search any keywords → get real sales leads**
|
|
33
|
+
|
|
34
|
+
**Works with any AI API** — OpenAI, Groq, Gemini, DeepSeek, OpenRouter, Mistral, Together, custom OpenAI-compatible servers, and more.
|
|
35
|
+
|
|
36
|
+
**Developed by Gyan Ranjan**
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install leadgen-ai
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
From GitHub:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install git+https://github.com/Gyan4589/leadgen-ai.git
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Setup (any API key)
|
|
55
|
+
|
|
56
|
+
Create a `.env` or set an environment variable. **Use the key for the provider you choose:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# OpenAI
|
|
60
|
+
set OPENAI_API_KEY=sk-...
|
|
61
|
+
|
|
62
|
+
# or Groq (fast + free tier)
|
|
63
|
+
set GROQ_API_KEY=gsk_...
|
|
64
|
+
|
|
65
|
+
# or Google Gemini
|
|
66
|
+
set GEMINI_API_KEY=...
|
|
67
|
+
|
|
68
|
+
# or DeepSeek / OpenRouter / Mistral
|
|
69
|
+
set DEEPSEEK_API_KEY=...
|
|
70
|
+
set OPENROUTER_API_KEY=...
|
|
71
|
+
set MISTRAL_API_KEY=...
|
|
72
|
+
|
|
73
|
+
# or generic
|
|
74
|
+
set LEADGEN_API_KEY=...
|
|
75
|
+
set LEADGEN_BASE_URL=https://api.openai.com/v1
|
|
76
|
+
set LEADGEN_MODEL=gpt-4o
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# list popular models
|
|
85
|
+
leadgen models
|
|
86
|
+
|
|
87
|
+
# search keywords
|
|
88
|
+
leadgen search "dental clinics Mumbai" -n 8
|
|
89
|
+
|
|
90
|
+
# pick a famous model
|
|
91
|
+
leadgen search "real estate agents Texas" -m gpt-4o -n 5
|
|
92
|
+
leadgen search "AI startups Bangalore" -m gemini-2.0-flash
|
|
93
|
+
leadgen search "gyms Delhi" -p groq -m llama-3.3-70b
|
|
94
|
+
leadgen search "HR SaaS USA" -m claude-3.5-sonnet # via OpenRouter key
|
|
95
|
+
|
|
96
|
+
# custom OpenAI-compatible endpoint (Ollama, vLLM, Azure proxy, etc.)
|
|
97
|
+
leadgen search "cafes NYC" --provider custom --base-url http://localhost:11434/v1 --api-key ollama -m llama3.2
|
|
98
|
+
|
|
99
|
+
# interactive
|
|
100
|
+
leadgen
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Popular models
|
|
106
|
+
|
|
107
|
+
| Shortcut | Provider |
|
|
108
|
+
|----------|----------|
|
|
109
|
+
| `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, `o3-mini` | OpenAI |
|
|
110
|
+
| `gemini-2.0-flash`, `gemini-1.5-pro` | Google |
|
|
111
|
+
| `llama-3.3-70b` | Groq |
|
|
112
|
+
| `deepseek-chat` | DeepSeek |
|
|
113
|
+
| `mistral-large` | Mistral |
|
|
114
|
+
| `claude-3.5-sonnet`, `claude-sonnet-4` | OpenRouter |
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
leadgen models
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## How it works
|
|
123
|
+
|
|
124
|
+
1. You type **keywords**
|
|
125
|
+
2. Live **web search** finds public company pages
|
|
126
|
+
3. Your chosen **LLM** scores leads + drafts outreach
|
|
127
|
+
4. Exports **CSV / JSON** to `output/`
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Flags
|
|
132
|
+
|
|
133
|
+
| Flag | Meaning |
|
|
134
|
+
|------|---------|
|
|
135
|
+
| `-n / --count` | Max leads |
|
|
136
|
+
| `-m / --model` | Model name or famous shortcut |
|
|
137
|
+
| `-p / --provider` | openai, groq, gemini, deepseek, openrouter, mistral, custom |
|
|
138
|
+
| `--base-url` | Custom API base URL |
|
|
139
|
+
| `--api-key` | Pass key on CLI (prefer env for safety) |
|
|
140
|
+
| `-g / --geo` | Geography filter |
|
|
141
|
+
| `--min-score` | Drop weak leads |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Ethics
|
|
146
|
+
|
|
147
|
+
- Public sources only
|
|
148
|
+
- No invented private emails
|
|
149
|
+
- Verify before outreach
|
|
150
|
+
- Respect spam / privacy laws
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Credits
|
|
155
|
+
|
|
156
|
+
**LeadGen AI**
|
|
157
|
+
**Developed by Gyan Ranjan**
|
|
158
|
+
https://github.com/Gyan4589/leadgen-ai
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# LeadGen AI
|
|
2
|
+
|
|
3
|
+
**Search any keywords → get real sales leads**
|
|
4
|
+
|
|
5
|
+
**Works with any AI API** — OpenAI, Groq, Gemini, DeepSeek, OpenRouter, Mistral, Together, custom OpenAI-compatible servers, and more.
|
|
6
|
+
|
|
7
|
+
**Developed by Gyan Ranjan**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install leadgen-ai
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
From GitHub:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install git+https://github.com/Gyan4589/leadgen-ai.git
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Setup (any API key)
|
|
26
|
+
|
|
27
|
+
Create a `.env` or set an environment variable. **Use the key for the provider you choose:**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# OpenAI
|
|
31
|
+
set OPENAI_API_KEY=sk-...
|
|
32
|
+
|
|
33
|
+
# or Groq (fast + free tier)
|
|
34
|
+
set GROQ_API_KEY=gsk_...
|
|
35
|
+
|
|
36
|
+
# or Google Gemini
|
|
37
|
+
set GEMINI_API_KEY=...
|
|
38
|
+
|
|
39
|
+
# or DeepSeek / OpenRouter / Mistral
|
|
40
|
+
set DEEPSEEK_API_KEY=...
|
|
41
|
+
set OPENROUTER_API_KEY=...
|
|
42
|
+
set MISTRAL_API_KEY=...
|
|
43
|
+
|
|
44
|
+
# or generic
|
|
45
|
+
set LEADGEN_API_KEY=...
|
|
46
|
+
set LEADGEN_BASE_URL=https://api.openai.com/v1
|
|
47
|
+
set LEADGEN_MODEL=gpt-4o
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# list popular models
|
|
56
|
+
leadgen models
|
|
57
|
+
|
|
58
|
+
# search keywords
|
|
59
|
+
leadgen search "dental clinics Mumbai" -n 8
|
|
60
|
+
|
|
61
|
+
# pick a famous model
|
|
62
|
+
leadgen search "real estate agents Texas" -m gpt-4o -n 5
|
|
63
|
+
leadgen search "AI startups Bangalore" -m gemini-2.0-flash
|
|
64
|
+
leadgen search "gyms Delhi" -p groq -m llama-3.3-70b
|
|
65
|
+
leadgen search "HR SaaS USA" -m claude-3.5-sonnet # via OpenRouter key
|
|
66
|
+
|
|
67
|
+
# custom OpenAI-compatible endpoint (Ollama, vLLM, Azure proxy, etc.)
|
|
68
|
+
leadgen search "cafes NYC" --provider custom --base-url http://localhost:11434/v1 --api-key ollama -m llama3.2
|
|
69
|
+
|
|
70
|
+
# interactive
|
|
71
|
+
leadgen
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Popular models
|
|
77
|
+
|
|
78
|
+
| Shortcut | Provider |
|
|
79
|
+
|----------|----------|
|
|
80
|
+
| `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, `o3-mini` | OpenAI |
|
|
81
|
+
| `gemini-2.0-flash`, `gemini-1.5-pro` | Google |
|
|
82
|
+
| `llama-3.3-70b` | Groq |
|
|
83
|
+
| `deepseek-chat` | DeepSeek |
|
|
84
|
+
| `mistral-large` | Mistral |
|
|
85
|
+
| `claude-3.5-sonnet`, `claude-sonnet-4` | OpenRouter |
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
leadgen models
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## How it works
|
|
94
|
+
|
|
95
|
+
1. You type **keywords**
|
|
96
|
+
2. Live **web search** finds public company pages
|
|
97
|
+
3. Your chosen **LLM** scores leads + drafts outreach
|
|
98
|
+
4. Exports **CSV / JSON** to `output/`
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Flags
|
|
103
|
+
|
|
104
|
+
| Flag | Meaning |
|
|
105
|
+
|------|---------|
|
|
106
|
+
| `-n / --count` | Max leads |
|
|
107
|
+
| `-m / --model` | Model name or famous shortcut |
|
|
108
|
+
| `-p / --provider` | openai, groq, gemini, deepseek, openrouter, mistral, custom |
|
|
109
|
+
| `--base-url` | Custom API base URL |
|
|
110
|
+
| `--api-key` | Pass key on CLI (prefer env for safety) |
|
|
111
|
+
| `-g / --geo` | Geography filter |
|
|
112
|
+
| `--min-score` | Drop weak leads |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Ethics
|
|
117
|
+
|
|
118
|
+
- Public sources only
|
|
119
|
+
- No invented private emails
|
|
120
|
+
- Verify before outreach
|
|
121
|
+
- Respect spam / privacy laws
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Credits
|
|
126
|
+
|
|
127
|
+
**LeadGen AI**
|
|
128
|
+
**Developed by Gyan Ranjan**
|
|
129
|
+
https://github.com/Gyan4589/leadgen-ai
|
|
@@ -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
|
+
)
|
|
@@ -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
|
+
)
|
|
@@ -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}
|