Encontrador de RP de Concorrentes
Forneça a URL do seu produto. Ele encontra seus concorrentes, pesquisa todos os canais de RP que eles usaram (notícias, podcasts, comunidades), revela os canais que aparecem em vários concorrentes (seus alvos comprovados), encontra o jornalista ou apresentador de cada um e elabora um pitch frio personalizado para o seu produto em cada canal de nível 1.
Política de zero alucinação: Cada canal, nome de jornalista, ângulo de história e detalhe do pitch na saída deve ser rastreável até um resultado de pesquisa específico do Tavily ou a página do produto buscada. Isso se aplica a:
- Nomes de concorrentes: devem aparecer nos resultados de pesquisa do Tavily, não no conhecimento de treinamento da IA
- Nomes de canais: devem ter uma URL nos resultados de pesquisa
- Nomes de jornalistas/apresentadores: devem aparecer textualmente em um snippet do Tavily
- Ângulos de história: extraídos apenas de títulos de artigos/episódios nos resultados de pesquisa
- Rascunhos de pitches: referenciam evidências específicas dos dados de pesquisa + análise do produto
Erros Comuns
| O agente vai querer... | Por que isso está errado |
|---|---|
| Nomear um jornalista a partir do conhecimento de treinamento | Cada nome de jornalista deve ser rastreável a um snippet de resultado de pesquisa. Escrever "Sarah Perez cobre startups no TechCrunch" de memória é alucinação. |
| Listar canais sem URLs de evidência | Cada canal na saída deve ter pelo menos uma URL dos resultados da pesquisa de RP comprovando que um concorrente foi apresentado lá. |
| Pular a etapa de confirmação do concorrente | Sempre mostre os concorrentes descobertos e aguarde a confirmação do usuário. Concorrentes errados = pesquisas desperdiçadas e uma saída inútil. |
| Gerar pitches genéricos ("Adoraríamos ser apresentados") | Cada pitch deve referenciar um ângulo específico da evidência E um diferenciador específico da análise do produto. |
| Marcar um canal como Nível 1 com apenas 1 ocorrência de concorrente | Nível 1 = 3+ concorrentes. Nível 2 = exatamente 2. Nível 3 = 1. Não promova canais que não se provaram. |
| Usar travessões na saída | Substitua todos os travessões (—) por hífens (-). |
Leia os Arquivos de Referência Antes de Cada Execução
cat references/pr-channel-types.md
cat references/pitch-guide.md
cat references/tier-scoring.md
Etapa 1: Verificação de Configuração
echo "TAVILY_API_KEY: ${TAVILY_API_KEY:+set}${TAVILY_API_KEY:-NÃO DEFINIDA -- obrigatório}"
echo "FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:+set}${FIRECRAWL_API_KEY:-não definida, o Tavily extract será usado como fallback}"
Se TAVILY_API_KEY estiver faltando: Pare imediatamente. Diga ao usuário: "TAVILY_API_KEY é necessária para pesquisar concorrentes e encontrar cobertura de RP. Não há fallback. Obtenha-a em app.tavily.com -- nível gratuito: 1000 créditos/mês (cerca de 43 execuções completas a ~23 pesquisas/execução). Adicione-a ao seu arquivo .env."
Se apenas FIRECRAWL_API_KEY estiver faltando: Continue. O Tavily extract será usado para a busca da URL.
Etapa 2: Analisar Entrada
Colete da conversa:
product_url: a URL para buscar (obrigatório, a menos que o usuário cole uma descrição diretamente)product_name: opcional, derivado da página se não fornecidogeography: opcional -- EUA / Europa / global. Padrão: EUA
Se o usuário fornecer apenas uma descrição colada (sem URL): Pule as Etapas 3 e 4. Vá diretamente para a Etapa 4 (análise do produto) usando o texto colado como product_content. Defina page_source como user_description e anote em data_quality_flags.
Se nem URL nem descrição: Pergunte: "Qual é a URL do seu produto ou startup? Ou cole uma breve descrição: o que faz, para quem é e o que o diferencia dos concorrentes."
Derive o slug do produto:
PRODUCT_SLUG=$(python3 -c "
from urllib.parse import urlparse
import sys
url = 'URL_AQUI'
if url.startswith('http'):
host = urlparse(url).netloc.replace('www.', '')
print(host.split('.')[0])
else:
import re
print(re.sub(r'[^a-z0-9]', '-', url[:30].lower()).strip('-'))
")
echo "Slug do produto: $PRODUCT_SLUG"
Etapa 3: Buscar Página do Produto
Principal: Firecrawl (se FIRECRAWL_API_KEY estiver definida)
curl -s -X POST https://api.firecrawl.dev/v1/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "URL_AQUI", "formats": ["markdown"], "onlyMainContent": true}' \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('data', {}).get('markdown', '') or d.get('markdown', '')
print(f'Buscado via Firecrawl: {len(content)} caracteres')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
Fallback: Tavily extract (se FIRECRAWL_API_KEY não estiver definida)
curl -s -X POST https://api.tavily.com/extract \
-H "Content-Type: application/json" \
-d "{\"api_key\": \"$TAVILY_API_KEY\", \"urls\": [\"URL_AQUI\"]}" \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('results', [{}])[0].get('raw_content', '')
print(f'Buscado via Tavily extract: {len(content)} caracteres')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
Ponto de verificação:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()
if len(content) < 200:
print('ERRO: menos de 200 caracteres buscados')
else:
print(f'Conteúdo OK: {len(content)} caracteres')
"
Se o conteúdo < 200 caracteres: Pare a busca. Diga ao usuário: "A página do produto não retornou conteúdo legível -- o site provavelmente é renderizado por JavaScript e bloqueou a busca. Cole uma breve descrição diretamente: o que faz, para quem é e o que o diferencia."
Etapa 4: Análise do Produto (IA)
Imprimir o conteúdo da página:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()[:5000]
print('=== PÁGINA DO PRODUTO (primeiros 5000 caracteres) ===')
print(content)
"
Instruções de IA: Analise a página do produto acima e extraia:
product_name: o nome do produto ou empresaone_line_description: o que faz, para quem, proposta de valor principal. Menos de 20 palavras. Sem linguagem de marketing. Exemplo: "Automação de CI/CD para equipes de desenvolvedores que auto-hospedam seus pipelines."industry_taxonomy:l1(nível superior: ex. ferramentas de desenvolvedor / fintech / healthtech / consumidor),l2(setor: ex. devops / pagamentos / telemedicina),l3(nicho específico: ex. automação de CI/CD / pagamentos incorporados / consulta por vídeo assíncrona). Rótulos vagos como "tecnologia" sozinhos não são aceitáveis.differentiators: exatamente 2-3 coisas específicas que distinguem este produto de concorrentes genéricos. Eles alimentam diretamente os rascunhos de pitches -- seja específico. Exemplo: ["Runner de pipeline auto-hospedado -- nenhum dado sai da sua infraestrutura", "Suporte nativo para monorepos com geração dinâmica de etapas"]icp:buyer_persona(cargo),company_type,company_sizegeography_bias: EUA / Europa / global / não claropage_source: "live_page" ou "user_description"
Escreva em /tmp/cprf-product-analysis.json:
python3 << 'PYEOF'
import json
analysis = {
# PREENCHA a partir de sua análise acima
"product_name": "",
"one_line_description": "",
"industry_taxonomy": {"l1": "", "l2": "", "l3": ""},
"differentiators": [],
"icp": {"buyer_persona": "", "company_type": "", "company_size": ""},
"geography_bias": "EUA",
"page_source": "live_page"
}
json.dump(analysis, open('/tmp/cprf-product-analysis.json', 'w'), indent=2)
print('Análise do produto escrita.')
PYEOF
Verifique:
python3 -c "
import json
a = json.load(open('/tmp/cprf-product-analysis.json'))
print('Produto:', a['product_name'])
print('Indústria:', a['industry_taxonomy']['l1'], '>', a['industry_taxonomy']['l2'], '>', a['industry_taxonomy']['l3'])
print('Diferenciadores:')
for d in a['differentiators']:
print(f' - {d}')
"
Etapa 4b: Fase 1 -- Descoberta de Concorrentes
ls scripts/research.py 2>/dev/null && echo "script encontrado" || echo "ERRO: scripts/research.py não encontrado -- não é possível continuar"
python3 scripts/research.py \
--phase discover \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-competitors-raw.json
Imprima os resultados para revisão da IA:
python3 -c "
import json
data = json.load(open('/tmp/cprf-competitors-raw.json'))
print(f'Pesquisas executadas: {len(data[\"competitor_searches\"])}')
for s in data['competitor_searches']:
print(f'\nConsulta: {s[\"query\"]}')
print(f'Resposta: {s.get(\"answer\",\"\")[:400]}')
for r in s.get('results', [])[:5]:
print(f' - {r[\"title\"]} | {r[\"url\"]}')
print(f' {r.get(\"content\",\"\")[:200]}')
"
Instruções de IA: Leia os resultados de pesquisa acima. Escolha exatamente 5 empresas concorrentes que:
- Estejam nomeadas nos títulos, respostas ou snippets dos resultados de pesquisa
- Estejam no mesmo nicho L3 que o produto que está sendo analisado
- Sejam produtos concorrentes reais (não agências, consultorias ou artigos de lista)
- Sejam distintas umas das outras (não a mesma empresa sob nomes diferentes)
Para cada concorrente, escreva: name, url (do resultado de pesquisa onde apareceu), description (uma frase do snippet), source_url (a URL do resultado de pesquisa onde foram encontrados).
Etapa 5: Confirmação do Concorrente
Mostre os concorrentes descobertos ao usuário:
python3 << 'PYEOF'
import json
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
# PREENCHA: 5 concorrentes dos resultados de pesquisa acima
candidates = [
# {"name": str, "url": str, "description": str, "source_url": str}
]
print(f"\nEncontrados 5 concorrentes para {analysis['product_name']} em {analysis['industry_taxonomy']['l3']}:\n")
for i, c in enumerate(candidates, 1):
print(f" {i}. {c['name']} -- {c['description']}")
print(f" {c['url']}")
data = json.load(open('/tmp/cprf-competitors-raw.json'))
data['competitor_candidates'] = candidates
json.dump(data, open('/tmp/cprf-competitors-raw.json', 'w'), indent=2)
PYEOF
Diga ao usuário: "Estes são os 5 concorrentes que pesquisarei para cobertura de RP. Adicione, remova ou troque qualquer um -- ou diga 'parece bom' para continuar."
Aguarde a confirmação. Se o usuário editar a lista (adicionar/remover/trocar), atualize os candidatos de acordo. Em seguida, escreva a lista confirmada:
python3 << 'PYEOF'
import json
# PREENCHA: lista de concorrentes confirmada (após revisão do usuário)
confirmed = [
# {"name": str, "url": str}
]
json.dump({"confirmed_competitors": confirmed}, open('/tmp/cprf-competitors-confirmed.json', 'w'), indent=2)
print(f"Confirmados {len(confirmed)} concorrentes para pesquisa de RP.")
for c in confirmed:
print(f" - {c['name']} ({c['url']})")
PYEOF
Etapa 6: Pesquisa de RP em Três Frentes (Fase 2)
python3 scripts/research.py \
--phase pr-research \
--competitors /tmp/cprf-competitors-confirmed.json \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-pr-raw.json
Isso executa 3 pesquisas por concorrente (15 no total):
- Trilha A (Editorial):
"[concorrente]" featured press coverage TechCrunch Forbes Wired article interview - Trilha B (Podcasts):
"[concorrente]" founder CEO podcast interview appeared on episode - Trilha C (Comunidades):
"[concorrente]" site:reddit.com OR site:news.ycombinator.com OR site:producthunt.com
Imprima o resumo da cobertura:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
print(f'Concorrentes pesquisados: {data[\"competitors_researched\"]}')
print()
for r in data['results']:
print(f'{r[\"competitor\"]}:')
for track, tdata in r['tracks'].items():
n = len(tdata.get('results', []))
print(f' {track:12}: {n} resultados')
"
Se todas as 3 trilhas para um concorrente retornarem 0 resultados: Este concorrente tem cobertura de imprensa muito baixa. Anote em data_quality_flags e prossiga -- o padrão entre concorrentes ainda funcionará com os 4 restantes.
Etapa 7: Análise de Padrões (IA)
Imprima todos os resultados brutos de RP:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
for r in data['results']:
print(f'\n=== {r[\"competitor\"]} ===')
for track, tdata in r['tracks'].items():
print(f'\n--- Trilha {track.upper()} ---')
print(f'Consulta: {tdata[\"query\"]}')
print(f'Resposta: {tdata.get(\"answer\",\"\")[:400]}')
for item in tdata.get('results', [])[:5]:
print(f' Título: {item[\"title\"]}')
print(f' URL: {item[\"url\"]}')
print(f' Snippet: {item.get(\"content\",\"\")[:200]}')
"
Instruções de IA: Leia TODOS os resultados de pesquisa acima. Construa um mapa de frequência de canais.
Passo 1 -- Normalizar URLs para domínio raiz: https://techcrunch.com/2023/06/article-title → techcrunch.com. https://open.spotify.com/episode/... → identificar como podcast (episódio do spotify). https://www.reddit.com/r/devops/ → reddit.com/r/devops.
Passo 2 -- Contar ocorrências: Quantos concorrentes diferentes apareceram nos resultados de cada canal raiz? Um canal que aparece nas Trilhas A do Concorrente A E nas Trilhas A do Concorrente B conta como frequência 2.
Passo 3 -- Hierarquizar canais (siga references/tier-scoring.md):
- Nível 1: apareceu em 3+ concorrentes
- Nível 2: apareceu em exatamente 2 concorrentes
- Nível 3: apareceu em 1 concorrente
Passo 4 -- Extrair ângulos de história dos títulos de artigos/episódios nos resultados. Classifique cada um como: anúncio-de-financiamento / lançamento-de-produto / história-do-fundador / artigo-de-tendência / criação-de-categoria / tutorial / comparação / prêmio. Não infira -- apenas classifique ângulos visíveis nos títulos.
Passo 5 -- Classificar tipo de canal para cada um: editorial / podcast / comunidade / newsletter.
Escreva em /tmp/cprf-pr-patterns.json:
python3 << 'PYEOF'
import json
patterns = {
"tier_1_channels": [
# PREENCHA -- canais que aparecem em 3+ concorrentes
# Cada: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"tier_2_channels": [
# PREENCHA -- canais que aparecem em exatamente 2 concorrentes
# Cada: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"tier_3_channels": [
# PREENCHA -- canais que aparecem em 1 concorrente
# Cada: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"data_quality_flags": []
}
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('Padrões de PR escritos.')
PYEOF
...
A saída foi truncada, mas o fluxo continua para gerar a lista de divulgação final e os rascunhos de pitch.