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 +6 -0
- agent/branding.py +15 -0
- agent/config.py +133 -0
- agent/export.py +148 -0
- agent/llm.py +94 -0
- agent/models.py +64 -0
- agent/models_catalog.py +97 -0
- agent/pipeline.py +330 -0
- agent/search.py +67 -0
- leadgen_ai-1.2.0.dist-info/METADATA +158 -0
- leadgen_ai-1.2.0.dist-info/RECORD +16 -0
- leadgen_ai-1.2.0.dist-info/WHEEL +5 -0
- leadgen_ai-1.2.0.dist-info/entry_points.txt +3 -0
- leadgen_ai-1.2.0.dist-info/licenses/LICENSE +21 -0
- leadgen_ai-1.2.0.dist-info/top_level.txt +2 -0
- main.py +455 -0
agent/__init__.py
ADDED
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 = ""
|
agent/models_catalog.py
ADDED
|
@@ -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)
|