Інструменти Cold Leads для агентів CrewAI і LangChain
- Для кого
- Python-розробники, які створюють агентів на CrewAI або LangChain
- Проблема
- Агент, якого просять знайти контактні дані, вгадуватиме, а вгадана адреса виглядає точнісінько як справжня. Без інструмента перевірки ні агент, ні людина, яка читає його відповідь, не відрізнить одну від іншої.
- Рішення
- Дайте агентові три вузькі інструменти на основі API Cold Leads: пошук у власній CRM користувача, перевірку адреси та припущення адреси за іменем і доменом із чесною позначкою методу. Помилки повертаються як дані, тож агент може на них реагувати, а не падати.
- Що ви отримаєте
- Спільний API-клієнт, класи інструментів для CrewAI та LangChain і мінімальні crew та агент, які ними користуються.
Адреси, ідентифікатори та результати в прикладах ілюстративні. Домен example.com зарезервовано для документації, тож реальна перевірка цих адрес поверне invalid.
Приклади коду однакові для всіх мов, коментарі в них — англійською.
Три інструменти
| Інструмент | Ендпоінт | Вартість | Що отримує агент |
|---|---|---|---|
| search_crm_contacts | GET /api/v1/leads | Безкоштовно | Контакти, які вже є у вашій CRM Cold Leads, на домені компанії (до 50), зі стадією, тегами, верифікацією та do_not_contact. |
| verify_email | POST /api/v1/verify | 1 кредит | status, score, reasons, mx, catch_all, disposable і role. |
| find_email | POST /api/v1/find | 1 кредит | Найімовірніша адреса, method (verified, pattern або none), confidence і candidates. |
Cold Leads не має бази людей чи компаній. search_crm_contacts не може знаходити нових потенційних клієнтів; він повертає лише контакти, які користувач уже має. Напишіть про це в описі інструмента, інакше модель очікуватиме іншого.
Спільний клієнт
Обидва фреймворки використовують той самий невеликий клієнт. Він повертає кожну відповідь як JSON-рядок і перетворює помилки API (no_credits, rate_limited, bad_api_key) на дані, які модель може прочитати, замість того щоб кидати виняток.
# coldleads_tools.py - shared by the CrewAI and LangChain examples (pip install httpx)
import json
import os
import httpx
VERIFY_DESC = (
"Verify one e-mail address with Cold Leads (costs 1 credit). Returns status valid, risky or invalid, "
"a score from 0 to 100 and reason codes. The last reason says what was checked: ok means a mail server "
"accepted the mailbox, smtp_unreachable means the mailbox itself was not checked, catch_all means the "
"domain accepts any address, mailbox_missing, no_mx and syntax mean the address is invalid."
)
FIND_DESC = (
"Guess a person's work e-mail address from first name, last name and company domain (costs 1 credit). "
"Returns the most likely address, method (verified only when a mail server confirmed the mailbox, "
"otherwise pattern), confidence from 0 to 100 and candidates. Treat a pattern result as unconfirmed."
)
SEARCH_DESC = (
"Search the contacts already saved in the user's own Cold Leads CRM for a company domain (free). "
"role is an optional keyword matched against name, e-mail local part, tags, notes and contact type. "
"This is not a people database: it only returns contacts the user already has. "
"Never e-mail contacts whose do_not_contact is true."
)
class ColdLeadsClient:
def __init__(self, api_key=None, base_url="https://coldleads.app/api/v1"):
self.http = httpx.Client(
base_url=base_url,
headers={"x-api-key": api_key or os.environ["COLDLEADS_API_KEY"]},
timeout=40.0,
)
def _call(self, method, path, **kwargs) -> str:
r = self.http.request(method, path, **kwargs)
try:
data = r.json()
except ValueError:
data = {}
if r.status_code >= 400:
# hand API errors (no_credits, rate_limited with retry_after_seconds, ...) to the model as data
return json.dumps({"http_status": r.status_code, "error": f"http_{r.status_code}", **data})
return json.dumps(data)
def verify(self, email: str) -> str:
return self._call("POST", "/verify", json={"email": email, "timeout_ms": 10000})
def find(self, first: str, last: str, domain: str) -> str:
return self._call("POST", "/find", json={"first": first, "last": last, "domain": domain})
def search_crm(self, domain: str, role: str = "", limit: int = 10) -> str:
params = {"domain": domain, "limit": limit}
if role:
params["role"] = role
return self._call("GET", "/leads", params=params)CrewAI
# pip install crewai httpx (CrewAI needs Python 3.10-3.13)
from crewai import LLM, Agent, Crew, Task
from crewai.tools import BaseTool, EnvVar
from pydantic import BaseModel, Field
from coldleads_tools import FIND_DESC, SEARCH_DESC, VERIFY_DESC, ColdLeadsClient
client = ColdLeadsClient()
KEY = [EnvVar(name="COLDLEADS_API_KEY", description="Cold Leads secret API key (sk_...)", required=True)]
class VerifyInput(BaseModel):
email: str = Field(..., description="E-mail address to verify, e.g. anna@example.com")
class FindInput(BaseModel):
first: str = Field(..., description="First name")
last: str = Field(..., description="Last name")
domain: str = Field(..., description="Company domain, e.g. example.com")
class SearchInput(BaseModel):
domain: str = Field(..., description="Company domain, e.g. example.com")
role: str = Field("", description="Optional keyword such as ceo, sales or marketing")
limit: int = Field(10, ge=1, le=50, description="Maximum number of contacts")
class VerifyEmailTool(BaseTool):
name: str = "verify_email"
description: str = VERIFY_DESC
args_schema: type[BaseModel] = VerifyInput
env_vars: list[EnvVar] = KEY
def _run(self, email: str) -> str:
return client.verify(email)
class FindEmailTool(BaseTool):
name: str = "find_email"
description: str = FIND_DESC
args_schema: type[BaseModel] = FindInput
env_vars: list[EnvVar] = KEY
def _run(self, first: str, last: str, domain: str) -> str:
return client.find(first, last, domain)
class SearchCrmTool(BaseTool):
name: str = "search_crm_contacts"
description: str = SEARCH_DESC
args_schema: type[BaseModel] = SearchInput
env_vars: list[EnvVar] = KEY
def _run(self, domain: str, role: str = "", limit: int = 10) -> str:
return client.search_crm(domain, role, limit)
researcher = Agent(
role="B2B outreach researcher",
goal="Prepare a short list of contacts whose addresses were checked with Cold Leads",
backstory="You never invent contacts or addresses and you report what each check actually confirmed.",
tools=[SearchCrmTool(), VerifyEmailTool(), FindEmailTool()],
llm=LLM(model="anthropic/claude-opus-5-5", max_tokens=4096), # any tool-calling model CrewAI supports
)
task = Task(
description=(
"Find the contacts at {domain} in my Cold Leads CRM whose role keyword matches {role}. "
"Skip anyone with do_not_contact true, verify the others and report status and the last reason code."
),
expected_output="A table with e-mail, name, status, score and last reason code.",
agent=researcher,
)
if __name__ == "__main__":
result = Crew(agents=[researcher], tasks=[task]).kickoff(inputs={"domain": "example.com", "role": "cto"})
print(result.raw)- Кожен інструмент успадковує crewai.tools.BaseTool і задає name, description, args_schema (модель Pydantic) та _run. env_vars оголошує ключ, потрібний інструменту; сам клієнт читає COLDLEADS_API_KEY з оточення.
- Інструменти передаються агентові як екземпляри, а {domain} і {role} у Task заповнюються з kickoff(inputs=...).
- CrewAI потребує Python від 3.10 до 3.13. Рядок llm використовує Claude Opus 5.5; підійде будь-яка модель із викликом інструментів, яку підтримує CrewAI.
LangChain
# pip install -U "langchain[anthropic]" httpx (LangChain 1.x, Python 3.10+)
from langchain.agents import create_agent
from langchain.tools import tool
from pydantic import BaseModel, Field
from coldleads_tools import FIND_DESC, SEARCH_DESC, VERIFY_DESC, ColdLeadsClient
client = ColdLeadsClient()
class VerifyInput(BaseModel):
email: str = Field(description="E-mail address to verify, e.g. anna@example.com")
class FindInput(BaseModel):
first: str = Field(description="First name")
last: str = Field(description="Last name")
domain: str = Field(description="Company domain, e.g. example.com")
class SearchInput(BaseModel):
domain: str = Field(description="Company domain, e.g. example.com")
role: str = Field(default="", description="Optional keyword such as ceo, sales or marketing")
limit: int = Field(default=10, ge=1, le=50, description="Maximum number of contacts")
@tool("verify_email", description=VERIFY_DESC, args_schema=VerifyInput)
def verify_email(email: str) -> str:
return client.verify(email)
@tool("find_email", description=FIND_DESC, args_schema=FindInput)
def find_email(first: str, last: str, domain: str) -> str:
return client.find(first, last, domain)
@tool("search_crm_contacts", description=SEARCH_DESC, args_schema=SearchInput)
def search_crm_contacts(domain: str, role: str = "", limit: int = 10) -> str:
return client.search_crm(domain, role, limit)
agent = create_agent(
model="anthropic:claude-opus-5-5", # any tool-calling chat model in provider:model form
tools=[search_crm_contacts, verify_email, find_email],
system_prompt=(
"You help with B2B outreach research. Use only the Cold Leads tools for contact data and never invent "
"addresses. For every verification, report the status and the last reason code."
),
)
if __name__ == "__main__":
result = agent.invoke({"messages": [{"role": "user", "content": "List my CRM contacts at example.com tagged cto and verify their addresses."}]})
final = result["messages"][-1]
print("".join(block["text"] for block in final.content_blocks if block["type"] == "text"))- Інструменти використовують декоратор @tool з langchain.tools з явно заданими name, description та args_schema.
- create_agent з langchain.agents запускає цикл викликів інструментів. Він замінює create_react_agent з LangGraph, який застарів.
- Модель задається у форматі provider:model. Фінальне повідомлення може містити кілька блоків вмісту, тож приклад виводить лише текстові блоки.
Описи, за якими модель може діяти
- Вкажіть в описі вартість (1 кредит для verify_email і find_email, безкоштовно для пошуку в CRM), щоб модель знала, які виклики витрачають кредити.
- Поясніть останній код причини: ok означає, що поштовий сервер прийняв скриньку, smtp_unreachable — що скриньку не перевірено, catch_all — що домен приймає будь-яку адресу.
- Напишіть, що результат find_email з method pattern — це припущення і що контактам із do_not_contact true не можна надсилати листи.
- Тримайте інструменти вузькими. Агент, який уміє лише шукати, перевіряти й припускати, не зможе помилково надіслати лист чи змінити записи.
Ліміти та вартість
- verify_email і find_email коштують 1 кредит за виклик, зокрема й для результатів із 30-денного кешу; пошук у CRM безкоштовний.
- Клієнт просить на перевірку 10 секунд; якщо цього часу не вистачає, результат — risky з причиною timeout, і він не кешується.
- 120 запитів на хвилину на ключ. Цикл агента, який перевіряє багато адрес, може впертися в цей ліміт; помилка rate_limited, яку агент тоді отримає, містить retry_after_seconds.
- API входить у тариф Business: $99 на місяць із 10 000 кредитів на місяць; додаткові пакети по 1 000 кредитів коштують $5.
Питання
Чи може агент знайти нових потенційних клієнтів у компанії?
Ні. Пошук у CRM повертає лише контакти, які вже є у вашому робочому просторі Cold Leads, а для пошуку адреси потрібне ім’я людини. Cold Leads не надає даних про людей.
Чому інструменти повертають помилки, а не кидають винятки?
Виняток усередині інструмента може завершити роботу агента. Повернута як JSON помилка на кшталт no_credits чи rate_limited доходить до моделі, яка може повідомити про неї або зачекати замість збою.
Чи можна обійтися без написання інструментів?
Так, для перевірки й пошуку в CRM: MCP-сервер Cold Leads надає search_leads і verify_email будь-якому MCP-клієнту. Інструмента пошуку адреси в ньому немає.