Cold Leads

Інструменти Cold Leads для агентів CrewAI і LangChain

Для кого
Python-розробники, які створюють агентів на CrewAI або LangChain
Проблема
Агент, якого просять знайти контактні дані, вгадуватиме, а вгадана адреса виглядає точнісінько як справжня. Без інструмента перевірки ні агент, ні людина, яка читає його відповідь, не відрізнить одну від іншої.
Рішення
Дайте агентові три вузькі інструменти на основі API Cold Leads: пошук у власній CRM користувача, перевірку адреси та припущення адреси за іменем і доменом із чесною позначкою методу. Помилки повертаються як дані, тож агент може на них реагувати, а не падати.
Що ви отримаєте
Спільний API-клієнт, класи інструментів для CrewAI та LangChain і мінімальні crew та агент, які ними користуються.

Адреси, ідентифікатори та результати в прикладах ілюстративні. Домен example.com зарезервовано для документації, тож реальна перевірка цих адрес поверне invalid.

Приклади коду однакові для всіх мов, коментарі в них — англійською.

Три інструменти

ІнструментЕндпоінтВартістьЩо отримує агент
search_crm_contactsGET /api/v1/leadsБезкоштовноКонтакти, які вже є у вашій CRM Cold Leads, на домені компанії (до 50), зі стадією, тегами, верифікацією та do_not_contact.
verify_emailPOST /api/v1/verify1 кредитstatus, score, reasons, mx, catch_all, disposable і role.
find_emailPOST /api/v1/find1 кредитНайімовірніша адреса, method (verified, pattern або none), confidence і candidates.

Cold Leads не має бази людей чи компаній. search_crm_contacts не може знаходити нових потенційних клієнтів; він повертає лише контакти, які користувач уже має. Напишіть про це в описі інструмента, інакше модель очікуватиме іншого.

Спільний клієнт

Обидва фреймворки використовують той самий невеликий клієнт. Він повертає кожну відповідь як JSON-рядок і перетворює помилки API (no_credits, rate_limited, bad_api_key) на дані, які модель може прочитати, замість того щоб кидати виняток.

coldleads_tools.py
# 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

crewai_coldleads.py
# 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

langchain_coldleads.py
# 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-клієнту. Інструмента пошуку адреси в ньому немає.