feat: full-service agency pitch — outreach email + subject, richer Gemini brief

- Prompt now describes complete agency capabilities (everything web-related)
- Concrete pitch examples with business name + specific problem references
- New mandatory output fields: outreach_email (3-4 sentence ready-to-send ES)
  and email_subject (specific subject line)
- HOT/WARM/COLD scoring guide based on site deficiency count
- Modal: pitch box replaced with full outreach email + subject + Copy button
- max_output_tokens raised to 6000

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 08:34:37 +02:00
parent 6cea07f0f4
commit 88c27bfff5
2 changed files with 65 additions and 24 deletions

View File

@@ -141,31 +141,57 @@ Profiles found on site: {social_str}
=== WEB SEARCH RESULTS (use to find contacts, verify business identity) === === WEB SEARCH RESULTS (use to find contacts, verify business identity) ===
{(search_results or "No results.")[:600]} {(search_results or "No results.")[:600]}
=== INSTRUCTIONS === === WHO WE ARE ===
The client sells: web redesign, SEO, hosting migration, SSL renewal, We are a full-service digital agency. We handle EVERYTHING web-related for SMEs:
security audits, GDPR compliance, accessibility fixes, Google Ads, new website builds, redesigns, landing pages, e-commerce, CMS migrations, speed
maintenance contracts, AI tools for SMEs, GMB setup, social media management. optimisation, mobile responsiveness, SSL/security, SEO (on-page + technical +
local), Google Ads, Google My Business setup & optimisation, social media
management (Instagram, Facebook, LinkedIn, TikTok), GDPR compliance, cookie
banners, accessibility fixes, hosting migrations, email setup, maintenance
contracts, and AI-powered tools. No job is too small or too large.
RULES — you MUST follow all of these: === ASSESSMENT RULES ===
1. A placeholder / minimal / blank site (few words, no images, no CMS) is one of Look at EVERY aspect of the site — quality, age, CMS, performance, SEO, GDPR,
the BEST leads — they need a complete website build + all digital services. social presence, GMB, contacts, hosting — and identify ALL the problems AND
Score it lead_quality=HOT or WARM and write an enthusiastic pitch. opportunities. Then build the pitch around the most compelling angle for THIS
2. pitch_angle is MANDATORY. Never leave it empty. Write 1 punchy Spanish sentence specific business.
tailored to the business type. Even "Hola, su web necesita una renovación
completa — podemos tenerla lista en 2 semanas." is better than nothing. Lead scoring guide:
3. services_needed must list at LEAST 2 services. For a blank/placeholder site • HOT — blank/placeholder site, or ≥3 serious issues (expired SSL, no SEO,
always include "diseño web" and "posicionamiento SEO". no mobile, lorem ipsum, non-EU hosting, no GDPR, no social, site >3 yrs old)
4. Use the WEB SEARCH RESULTS to find the real phone/email — put the best one • WARM — functional but clearly outdated or missing 1-2 key services
in best_contact_value. • COLD — modern, well-maintained site with few obvious gaps
5. Use copyright_year + Last-Modified to estimate site_last_updated.
6. Keep every string value SHORT (≤ 15 words). Arrays: max 4 items. MANDATORY for EVERY assessment — no exceptions:
This keeps the JSON small and avoids truncation. 1. pitch_angle: A single, compelling cold-outreach sentence in Spanish, personalised
to this specific business name/type and its biggest weakness. Reference the actual
problem. Examples of good pitches:
- "Hola Salom Manacor, su web lleva sin actualizarse desde 2019 — en 3 semanas
le entregamos una nueva web con ficha en Google, redes sociales y posicionamiento
incluidos."
- "Detectamos que la web de [Negocio] no aparece en Google Maps ni tiene perfil
en Instagram — podemos solucionarlo esta semana."
- "Su certificado SSL vence en 12 días y su web no tiene aviso de cookies legal —
evite multas y pérdida de visitas con nuestro plan de mantenimiento."
2. outreach_email: A 3-4 sentence ready-to-send email in Spanish. First sentence
names the business and the most urgent problem. Second sentence explains the
impact (losing clients, Google ranking, legal risk). Third sentence introduces us
as the solution. Close with a call to action (llamada de 15 min, presupuesto
gratuito). Sign off: "Un saludo, [Agencia Digital]".
3. email_subject: A short, specific Spanish email subject line referencing the
business name and main issue (e.g. "Web de Salom Manacor — propuesta de mejora").
4. services_needed: At least 2 specific services from our catalogue.
5. Use WEB SEARCH RESULTS to find real phone/email for best_contact_value.
6. Use copyright_year + Last-Modified to estimate site_last_updated.
7. Keep all string values concise (≤ 20 words each). Arrays: max 4 items.
Respond ONLY with valid JSON, no markdown fences, no text outside the JSON: Respond ONLY with valid JSON, no markdown fences, no text outside the JSON:
{{ {{
"lead_quality": "HOT|WARM|COLD", "lead_quality": "HOT|WARM|COLD",
"lead_reasoning": "1-2 sentences why", "lead_reasoning": "1-2 sentences",
"pitch_angle": "1 punchy cold-outreach sentence in Spanish — NEVER empty", "pitch_angle": "1 punchy sentence in Spanish referencing the specific business problem",
"outreach_email": "ready-to-send 3-4 sentence email in Spanish",
"email_subject": "specific Spanish subject line",
"services_needed": ["service1","service2"], "services_needed": ["service1","service2"],
"best_contact_channel": "email|phone|whatsapp|social|web_form|unknown", "best_contact_channel": "email|phone|whatsapp|social|web_form|unknown",
"best_contact_value": "real email/phone from page or search results", "best_contact_value": "real email/phone from page or search results",
@@ -239,7 +265,7 @@ async def assess_domain(analysis: dict) -> dict:
"top_p": 0.9, "top_p": 0.9,
"temperature": 0.2, "temperature": 0.2,
"thinking_level": "low", "thinking_level": "low",
"max_output_tokens": 4096, "max_output_tokens": 6000,
} }
} }
try: try:

View File

@@ -237,10 +237,16 @@ tr:hover td{background:rgba(255,255,255,.025)}
</div> </div>
</div> </div>
<!-- Pitch --> <!-- Outreach email -->
<div style="background:#6c63ff15;border:1px solid #6c63ff33;border-radius:6px;padding:10px;margin:8px 0"> <div style="background:#6c63ff15;border:1px solid #6c63ff33;border-radius:6px;padding:10px;margin:8px 0">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;margin-bottom:4px">Cold Pitch (ES)</div> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
<div style="font-size:13px;font-style:italic;color:var(--accent2)" x-text="modal.ai.pitch_angle||'—'"></div> <div style="font-size:10px;color:var(--muted);text-transform:uppercase">Outreach Email (ES)</div>
<button class="btn bg sm" @click="copyEmail()" style="font-size:10px;padding:2px 8px">📋 Copy</button>
</div>
<div x-show="modal.ai.email_subject" style="font-size:11px;color:var(--muted);margin-bottom:4px">
<b>Subject:</b> <span x-text="modal.ai.email_subject"></span>
</div>
<div style="font-size:12px;font-style:italic;color:var(--accent2);line-height:1.5;white-space:pre-wrap" x-text="modal.ai.outreach_email || modal.ai.pitch_angle || '—'"></div>
</div> </div>
<!-- Accessibility issues --> <!-- Accessibility issues -->
@@ -657,6 +663,15 @@ function app() {
}, },
async restartAiWorker() { await fetch('/api/ai/worker/restart',{method:'POST'}); this.notify('AI worker restarted','info'); await this.loadAiStatus(); }, async restartAiWorker() { await fetch('/api/ai/worker/restart',{method:'POST'}); this.notify('AI worker restarted','info'); await this.loadAiStatus(); },
copyEmail() {
const subj = this.modal.ai.email_subject ? `Subject: ${this.modal.ai.email_subject}\n\n` : '';
const body = this.modal.ai.outreach_email || this.modal.ai.pitch_angle || '';
navigator.clipboard.writeText(subj + body).then(
() => this.notify('Email copied to clipboard', 'success'),
() => this.notify('Copy failed — select text manually', 'error'),
);
},
async resetAiStuck() { const r=await fetch('/api/ai/reset',{method:'POST'}); const d=await r.json(); this.notify(`Reset ${d.reset} stuck jobs → pending`,'success'); await this.loadAiStatus(); }, async resetAiStuck() { const r=await fetch('/api/ai/reset',{method:'POST'}); const d=await r.json(); this.notify(`Reset ${d.reset} stuck jobs → pending`,'success'); await this.loadAiStatus(); },
async startEnrich() { await fetch('/api/enrich/resume',{method:'POST'}); this.notify('Worker started','success'); await this.loadQueue(); }, async startEnrich() { await fetch('/api/enrich/resume',{method:'POST'}); this.notify('Worker started','success'); await this.loadQueue(); },
async pauseEnrich() { await fetch('/api/enrich/pause',{method:'POST'}); this.notify('Worker paused','success'); await this.loadQueue(); }, async pauseEnrich() { await fetch('/api/enrich/pause',{method:'POST'}); this.notify('Worker paused','success'); await this.loadQueue(); },